33import subprocess
44import threading
55import platform
6+ import tempfile
7+ import shutil
68from datetime import datetime , timedelta
9+ from pathlib import Path
710from typing import Dict , Set , List
811from colorama import Fore , Style
912from utils .logger import get_logger
@@ -21,6 +24,10 @@ def __init__(self, block_duration: int, whitelist: Set[str]):
2124 self .logger = get_logger (__name__ )
2225 self .platform = platform .system ().lower ()
2326 self .firewall_cmd = get_platform_firewall_command ()
27+
28+ # macOS-specific: pfctl configuration
29+ if self .platform == 'darwin' :
30+ self ._init_macos_firewall ()
2431
2532 def block_ip (self , ip : str , reason : str ) -> bool :
2633 """Block an IP address using the appropriate firewall system"""
@@ -138,23 +145,114 @@ def _unblock_ip_linux(self, ip: str) -> bool:
138145 result = subprocess .run (cmd , capture_output = True , text = True )
139146 return result .returncode == 0
140147
148+ def _init_macos_firewall (self ):
149+ """Initialize macOS firewall (pfctl) - create table and rules"""
150+ try :
151+ # Check if pfctl is available
152+ pfctl_path = shutil .which ('pfctl' )
153+ if not pfctl_path :
154+ self .logger .warning ("pfctl not found." )
155+ return
156+
157+ cmd = ['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'show' ]
158+ result = subprocess .run (cmd , capture_output = True , text = True )
159+ if result .returncode != 0 :
160+ # if doesn't exist, create dummy
161+ subprocess .run (['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'add' , '127.0.0.1' ],
162+ capture_output = True , text = True )
163+ subprocess .run (['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'delete' , '127.0.0.1' ],
164+ capture_output = True , text = True )
165+
166+ cmd = ['sudo' , 'pfctl' , '-s' , 'info' ]
167+ result = subprocess .run (cmd , capture_output = True , text = True )
168+ if 'Status: Enabled' not in result .stdout :
169+ # Try to enable pfctl (may require user interaction)
170+ self .logger .info ("Attempting to enable pfctl..." )
171+ subprocess .run (['sudo' , 'pfctl' , '-e' ], capture_output = True , text = True )
172+
173+ self ._reload_macos_rules ()
174+
175+ self .logger .info ("macOS firewall initialized successfully" )
176+ except Exception as e :
177+ self .logger .error (f"Failed to initialize macOS firewall: { e } " )
178+
179+ def _reload_macos_rules (self ):
180+ """Reload pfctl rules to ensure blocking rule is active"""
181+ try :
182+
183+ pf_conf_content = """# Simple Firewall - Dynamic IP Blocking Rules
184+ # This file is managed by Simple Firewall
185+ # DO NOT EDIT MANUALLY
186+
187+ # Table for blocked IPs
188+ table <blocked_ips> persist
189+
190+ # Block all traffic from IPs in the blocked_ips table
191+ block drop in quick from <blocked_ips> to any
192+ block drop out quick from any to <blocked_ips>
193+
194+ # Allow all other traffic (pass through)
195+ pass in all
196+ pass out all
197+ """
198+ temp_dir = Path (tempfile .gettempdir ())
199+ pf_conf_path = temp_dir / 'simple_firewall_pf.conf'
200+
201+ with open (pf_conf_path , 'w' ) as f :
202+ f .write (pf_conf_content )
203+
204+ cmd = ['sudo' , 'pfctl' , '-f' , str (pf_conf_path )]
205+ result = subprocess .run (cmd , capture_output = True , text = True )
206+
207+ if result .returncode == 0 :
208+ self .logger .debug ("pfctl rules loaded successfully" )
209+ else :
210+ self .logger .warning (f"pfctl rule loading returned: { result .returncode } " )
211+ self .logger .debug (f"pfctl stderr: { result .stderr } " )
212+
213+ except Exception as e :
214+ self .logger .error (f"Failed to reload macOS firewall rules: { e } " )
215+
141216 def _block_ip_macos (self , ip : str ) -> bool :
142217 """Block IP using pfctl on macOS"""
143- # First, add IP to a table
144- cmd1 = ['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'add' , ip ]
145- result1 = subprocess .run (cmd1 , capture_output = True , text = True )
146-
147- # Then enable the blocking rule (this might need to be done once)
148- cmd2 = ['sudo' , 'pfctl' , '-e' ]
149- result2 = subprocess .run (cmd2 , capture_output = True , text = True )
150-
151- return result1 .returncode == 0
218+ try :
219+ # First, add IP to a table
220+ cmd = ['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'add' , ip ]
221+ result = subprocess .run (cmd , capture_output = True , text = True )
222+
223+ if result .returncode == 0 :
224+ self .logger .debug (f"Successfully added { ip } to blocked_ips table" )
225+ return True
226+ else :
227+
228+ if 'already' in result .stderr .lower () or 'duplicate' in result .stderr .lower ():
229+ self .logger .debug (f"IP { ip } already in blocked_ips table" )
230+ return True
231+ else :
232+ self .logger .error (f"Failed to add { ip } to blocked_ips table: { result .stderr } " )
233+ return False
234+ except Exception as e :
235+ self .logger .error (f"Exception while blocking IP { ip } on macOS: { e } " )
236+ return False
152237
153238 def _unblock_ip_macos (self , ip : str ) -> bool :
154239 """Unblock IP using pfctl on macOS"""
155- cmd = ['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'delete' , ip ]
156- result = subprocess .run (cmd , capture_output = True , text = True )
157- return result .returncode == 0
240+ try :
241+ cmd = ['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'delete' , ip ]
242+ result = subprocess .run (cmd , capture_output = True , text = True )
243+ if result .returncode == 0 :
244+ self .logger .debug (f"Successfully removed { ip } from blocked_ips table" )
245+ return True
246+ else :
247+ if 'not found' in result .stderr .lower () or 'does not exist' in result .stderr .lower ():
248+ self .logger .debug (f"IP { ip } not found in blocked_ips table (may have been already removed)" )
249+ return True
250+ else :
251+ self .logger .warning (f"Failed to remove { ip } from blocked_ips table: { result .stderr } " )
252+ return False
253+ except Exception as e :
254+ self .logger .error (f"Exception while unblocking IP { ip } on macOS: { e } " )
255+ return False
158256
159257 def _block_ip_windows (self , ip : str ) -> bool :
160258 """Block IP using Windows Firewall (netsh)"""
@@ -216,6 +314,16 @@ def cleanup_all_blocks(self) -> List[str]:
216314 if self .unblock_ip (ip ):
217315 cleaned_ips .append (ip )
218316
317+ # macOS-specific: Clean up the entire table on shutdown
318+ if self .platform == 'darwin' and cleaned_ips :
319+ try :
320+ cmd = ['sudo' , 'pfctl' , '-t' , 'blocked_ips' , '-T' , 'flush' ]
321+ result = subprocess .run (cmd , capture_output = True , text = True )
322+ if result .returncode == 0 :
323+ self .logger .info ("Cleaned up all blocked IPs from pfctl table" )
324+ except Exception as e :
325+ self .logger .warning (f"Failed to flush pfctl table: { e } " )
326+
219327 return cleaned_ips
220328
221329 def get_stats (self ) -> Dict [str , int ]:
0 commit comments