Skip to content

Commit 047a623

Browse files
author
Magnus
committed
Add IP blocking to Port Blocker dialog
1 parent 8bb459e commit 047a623

File tree

2 files changed

+144
-8
lines changed

2 files changed

+144
-8
lines changed

src/gui/main.py

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
from tools.utils_gui import set_settings, get_settings
2828
from tools.utils import goto, is_connected, get_default_iface
2929
from tools.pfctl import (ensure_pf_enabled, install_anchor, block_all_for, unblock_all_for,
30-
block_port, unblock_port, is_port_blocked, list_blocked_ports, clear_all_port_blocks)
30+
block_port, unblock_port, is_port_blocked, list_blocked_ports, clear_all_port_blocks,
31+
block_ip, unblock_ip, list_blocked_ips)
3132

3233
from assets import *
3334

@@ -207,6 +208,27 @@ def setup_ui(self):
207208

208209
layout.addWidget(quick_group)
209210

211+
# IP blocking section
212+
ip_group = QGroupBox('Block IP Address')
213+
ip_layout = QHBoxLayout(ip_group)
214+
215+
self.ipInput = QLineEdit()
216+
self.ipInput.setPlaceholderText('e.g. 192.168.1.100')
217+
ip_layout.addWidget(QLabel('IP:'))
218+
ip_layout.addWidget(self.ipInput)
219+
220+
self.ipDirCombo = QComboBox()
221+
self.ipDirCombo.addItems(['Both', 'In', 'Out'])
222+
ip_layout.addWidget(QLabel('Dir:'))
223+
ip_layout.addWidget(self.ipDirCombo)
224+
225+
self.blockIpBtn = QPushButton('Block IP')
226+
self.blockIpBtn.clicked.connect(self.block_ip_clicked)
227+
self.blockIpBtn.setStyleSheet('background-color: #c0392b; color: white;')
228+
ip_layout.addWidget(self.blockIpBtn)
229+
230+
layout.addWidget(ip_group)
231+
210232
# Common ports with checkboxes
211233
common_group = QGroupBox('Common Ports (Click to Toggle)')
212234
common_layout = QVBoxLayout(common_group)
@@ -268,6 +290,15 @@ def quick_block(self):
268290

269291
self.refresh_list()
270292

293+
def block_ip_clicked(self):
294+
ip = self.ipInput.text().strip()
295+
if not ip:
296+
return
297+
direction = self.ipDirCombo.currentText().lower()
298+
block_ip(self.iface, ip, direction)
299+
self.ipInput.clear()
300+
self.refresh_list()
301+
271302
def on_item_changed(self, item):
272303
port = item.data(Qt.UserRole)
273304
if item.checkState() == Qt.Checked:
@@ -297,26 +328,46 @@ def refresh_list(self):
297328
self.portList.blockSignals(False)
298329

299330
def refresh_blocked_list(self):
300-
"""Refresh just the blocked ports display."""
331+
"""Refresh just the blocked ports and IPs display."""
301332
self.blockedList.clear()
302-
blocked = list_blocked_ports()
333+
334+
# Add blocked ports
335+
blocked_ports = list_blocked_ports()
303336
seen = set()
304-
for port, proto, direction in blocked:
337+
for port, proto, direction in blocked_ports:
305338
key = (port, proto)
306339
if key not in seen:
307340
seen.add(key)
308-
item = QListWidgetItem(f'{port} ({proto.upper()}) - {direction}')
309-
item.setData(Qt.UserRole, (port, proto))
341+
item = QListWidgetItem(f'Port {port} ({proto.upper()}) - {direction}')
342+
item.setData(Qt.UserRole, ('port', port, proto))
343+
self.blockedList.addItem(item)
344+
345+
# Add blocked IPs
346+
blocked_ips = list_blocked_ips()
347+
seen_ips = set()
348+
for ip, direction in blocked_ips:
349+
if ip not in seen_ips:
350+
seen_ips.add(ip)
351+
item = QListWidgetItem(f'IP {ip} - {direction}')
352+
item.setData(Qt.UserRole, ('ip', ip))
310353
self.blockedList.addItem(item)
311354

312355
def unblock_selected(self):
313356
for item in self.blockedList.selectedItems():
314-
port, proto = item.data(Qt.UserRole)
315-
unblock_port(port, proto)
357+
data = item.data(Qt.UserRole)
358+
if data[0] == 'port':
359+
_, port, proto = data
360+
unblock_port(port, proto)
361+
elif data[0] == 'ip':
362+
_, ip = data
363+
unblock_ip(ip)
316364
self.refresh_list()
317365

318366
def clear_all(self):
319367
clear_all_port_blocks()
368+
# Also clear blocked IPs
369+
for ip, _ in list_blocked_ips():
370+
unblock_ip(ip)
320371
self.refresh_list()
321372

322373

src/tools/pfctl.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,91 @@ def pf_test_roundtrip(iface: str, victim_ip: str) -> bool:
193193
return ok1 and present and ok2
194194

195195

196+
# ============== IP BLOCKING ==============
197+
198+
def block_ip(iface: str, ip: str, direction: str = 'both') -> bool:
199+
"""Block all traffic to/from a specific IP."""
200+
if sys.platform == 'darwin':
201+
rules = []
202+
if direction in ('in', 'both'):
203+
rules.append(f'block drop quick in on {iface} from {ip} to any')
204+
if direction in ('out', 'both'):
205+
rules.append(f'block drop quick out on {iface} from any to {ip}')
206+
for rule in rules:
207+
_exec(f"sh -c 'echo " + '"' + f"{rule}" + '"' + f" >> {_anchor_file()}'")
208+
_exec(f"pfctl -a {ANCHOR} -f {_anchor_file()}")
209+
return True
210+
elif sys.platform.startswith('win'):
211+
rule_name = f'arpcut_ip_{ip.replace(".", "_")}'
212+
if direction in ('in', 'both'):
213+
_exec(f'netsh advfirewall firewall add rule name="{rule_name}_in" dir=in action=block remoteip={ip} enable=yes')
214+
if direction in ('out', 'both'):
215+
_exec(f'netsh advfirewall firewall add rule name="{rule_name}_out" dir=out action=block remoteip={ip} enable=yes')
216+
return True
217+
return False
218+
219+
220+
def unblock_ip(ip: str) -> bool:
221+
"""Remove IP blocking rules."""
222+
if sys.platform == 'darwin':
223+
try:
224+
path = _anchor_file()
225+
with open(path, 'r') as f:
226+
lines = f.readlines()
227+
with open(path, 'w') as f:
228+
for line in lines:
229+
if f'from {ip} ' not in line and f'to {ip}' not in line:
230+
f.write(line)
231+
_exec(f"pfctl -a {ANCHOR} -f {path}")
232+
return True
233+
except Exception:
234+
return False
235+
elif sys.platform.startswith('win'):
236+
rule_name = f'arpcut_ip_{ip.replace(".", "_")}'
237+
_exec(f'netsh advfirewall firewall delete rule name="{rule_name}_in"')
238+
_exec(f'netsh advfirewall firewall delete rule name="{rule_name}_out"')
239+
return True
240+
return False
241+
242+
243+
def list_blocked_ips() -> list:
244+
"""Return list of blocked IPs as [(ip, direction), ...]"""
245+
blocked = []
246+
if sys.platform == 'darwin':
247+
rules = list_rules()
248+
for rule in rules:
249+
if 'block drop quick' in rule and 'port' not in rule:
250+
parts = rule.split()
251+
try:
252+
if 'from' in parts and 'to' in parts:
253+
from_idx = parts.index('from')
254+
to_idx = parts.index('to')
255+
from_ip = parts[from_idx + 1]
256+
to_ip = parts[to_idx + 1]
257+
direction = 'in' if ' in on ' in rule else 'out'
258+
if from_ip != 'any':
259+
blocked.append((from_ip, direction))
260+
elif to_ip != 'any':
261+
blocked.append((to_ip, direction))
262+
except (ValueError, IndexError):
263+
pass
264+
elif sys.platform.startswith('win'):
265+
res = _exec('netsh advfirewall firewall show rule name=all')
266+
if res.returncode == 0:
267+
current_rule = {}
268+
for line in res.stdout.splitlines():
269+
line = line.strip()
270+
if line.startswith('Rule Name:'):
271+
name = line.split(':', 1)[1].strip()
272+
if 'arpcut_ip_' in name.lower():
273+
parts = name.split('_')
274+
if len(parts) >= 6:
275+
ip = '.'.join(parts[2:6])
276+
direction = parts[-1] if parts[-1] in ('in', 'out') else 'both'
277+
blocked.append((ip, direction))
278+
return blocked
279+
280+
196281
# ============== PORT BLOCKING ==============
197282

198283
def block_port(iface: str, port: int, proto: str = 'tcp', direction: str = 'both') -> bool:

0 commit comments

Comments
 (0)