From d07c2becf2d931d67630a2a2d0b8aa944ad2a4f1 Mon Sep 17 00:00:00 2001 From: indar suthar Date: Mon, 10 Nov 2025 02:06:07 +0530 Subject: [PATCH] feat: macos firewall support using pfctl --- README.md | 138 ++++++++++++++++++++++++++++++++++++++- src/firewall/blocking.py | 132 +++++++++++++++++++++++++++++++++---- 2 files changed, 256 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 520eb1d..a4b95bf 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,115 @@ This program uses `netsh` to manage firewall rules, which requires administrativ # Example: python main.py -i "Ethernet" ``` + +## 🍎 Running on macOS + +This program uses `pfctl` (Packet Filter Control) to manage firewall rules, which requires administrative privileges. + +### Prerequisites + +- **macOS 10.5+** +- **Administrator privileges** +- **Python 3.6+** + +### Setup Instructions + +1. **Open Terminal:** Open the Terminal application (found in Applications > Utilities) + +2. **Navigate to Project Directory:** + ```bash + cd path/to/simple_firewall + ``` + +3. **(Optional) Create Virtual Environment:** + ```bash + python3 -m venv venv + source venv/bin/activate + ``` + +4. **Install Requirements:** + ```bash + pip install -r requirements.txt + ``` + +5. **Create Configuration File:** + ```bash + python3 main.py --create-config + ``` + +6. **Run the Firewall:** + ```bash + sudo python3 main.py + ``` + + Or specify an interface: + ```bash + sudo python3 main.py -i en0 + ``` + +### macOS Firewall Management + +The firewall uses `pfctl` to manage blocking rules: + +- **Automatic Setup:** On first run, the firewall automatically: + - Creates a `blocked_ips` table in pfctl + - Enables pfctl if not already enabled + - Loads blocking rules from a temporary configuration file + +- **Blocking Mechanism:** IPs are added to a pfctl table, and rules block all traffic from/to those IPs + +- **Automatic Cleanup:** When the firewall stops, all blocked IPs are automatically removed + +### Managing pfctl Manually + +If you need to manually manage the firewall: + +```bash +# View currently blocked IPs +sudo pfctl -t blocked_ips -T show + +# Remove a specific IP manually +sudo pfctl -t blocked_ips -T delete + +# Flush all blocked IPs +sudo pfctl -t blocked_ips -T flush + +# Check pfctl status +sudo pfctl -s info + +# View current rules +sudo pfctl -s rules +``` + +### Troubleshooting macOS + +**Issue: "pfctl: command not found"** +- This shouldn't happen on macOS, but if it does, ensure you're running a supported macOS version + +**Issue: "pfctl: Permission denied"** +- Make sure you're running with `sudo` +- Some macOS versions may require additional permissions + +**Issue: "pfctl: cannot enable"** +- macOS System Preferences may have the built-in firewall enabled +- You may need to disable it in System Preferences > Security & Privacy > Firewall +- Or allow pfctl to run by granting Full Disk Access in System Preferences + +**Issue: Interface not found** +- List available interfaces: `ifconfig` or `networksetup -listallhardwareports` +- Common macOS interfaces: `en0` (Ethernet), `en1` (Wi-Fi), `en2` (USB Ethernet) + +**Issue: Blocks not working** +- Check if pfctl is enabled: `sudo pfctl -s info` +- Verify rules are loaded: `sudo pfctl -s rules` +- Check if IPs are in the table: `sudo pfctl -t blocked_ips -T show` + +### macOS-Specific Notes + +- **System Firewall:** macOS has a built-in firewall in System Preferences. The Simple Firewall works alongside it but uses pfctl directly +- **Interface Names:** macOS uses names like `en0`, `en1`, etc. (not `eth0` like Linux) +- **Root Access:** Always run with `sudo` for firewall operations +- **Temporary Rules:** The firewall creates temporary pf.conf files that are automatically managed ## Usage @@ -306,8 +415,11 @@ src/ ## Requirements - **Python 3.6+** -- **Root privileges** (required for iptables access) -- **Linux system** (uses iptables for blocking) +- **Root/Administrator privileges** (required for firewall access) +- **Supported Platforms:** + - **Linux:** Uses iptables for blocking + - **macOS:** Uses pfctl (Packet Filter) for blocking + - **Windows:** Uses netsh (Windows Firewall) for blocking ### Python Packages: - `scapy` - Packet capture and analysis @@ -389,6 +501,27 @@ sudo iptables -D INPUT -s [IP_ADDRESS] -j DROP sudo iptables -F INPUT ``` +**macOS (pfctl):** +```bash +# View blocked IPs in table +sudo pfctl -t blocked_ips -T show + +# Remove specific IP +sudo pfctl -t blocked_ips -T delete [IP_ADDRESS] + +# Flush all blocked IPs +sudo pfctl -t blocked_ips -T flush +``` + +**Windows (netsh):** +```powershell +# List firewall rules +netsh advfirewall firewall show rule name=all + +# Delete specific rule (replace IP_ADDRESS with actual IP) +netsh advfirewall firewall delete rule name="SimpleFirewall_Block_[IP_ADDRESS]" +``` + ## Current Limitations & Known Issues ⚠️ **Platform Limitations:** @@ -448,6 +581,7 @@ This is an **educational open-source project** designed to help people learn net - **Performance:** Optimize packet processing and memory usage - **Testing:** Add unit tests and integration tests - **Documentation:** Improve code comments and examples +- **IPv6 Support:** Extend blocking to IPv6 addresses ### 📋 **Current Architecture Issues to Fix:** - Replace hardcoded magic numbers with named constants diff --git a/src/firewall/blocking.py b/src/firewall/blocking.py index 7662572..ea9b115 100644 --- a/src/firewall/blocking.py +++ b/src/firewall/blocking.py @@ -3,7 +3,10 @@ import subprocess import threading import platform +import tempfile +import shutil from datetime import datetime, timedelta +from pathlib import Path from typing import Dict, Set, List from colorama import Fore, Style from utils.logger import get_logger @@ -21,6 +24,10 @@ def __init__(self, block_duration: int, whitelist: Set[str]): self.logger = get_logger(__name__) self.platform = platform.system().lower() self.firewall_cmd = get_platform_firewall_command() + + # macOS-specific: pfctl configuration + if self.platform == 'darwin': + self._init_macos_firewall() def block_ip(self, ip: str, reason: str) -> bool: """Block an IP address using the appropriate firewall system""" @@ -138,23 +145,114 @@ def _unblock_ip_linux(self, ip: str) -> bool: result = subprocess.run(cmd, capture_output=True, text=True) return result.returncode == 0 + def _init_macos_firewall(self): + """Initialize macOS firewall (pfctl) - create table and rules""" + try: + # Check if pfctl is available + pfctl_path = shutil.which('pfctl') + if not pfctl_path: + self.logger.warning("pfctl not found.") + return + + cmd = ['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'show'] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + # if doesn't exist, create dummy + subprocess.run(['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'add', '127.0.0.1'], + capture_output=True, text=True) + subprocess.run(['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'delete', '127.0.0.1'], + capture_output=True, text=True) + + cmd = ['sudo', 'pfctl', '-s', 'info'] + result = subprocess.run(cmd, capture_output=True, text=True) + if 'Status: Enabled' not in result.stdout: + # Try to enable pfctl (may require user interaction) + self.logger.info("Attempting to enable pfctl...") + subprocess.run(['sudo', 'pfctl', '-e'], capture_output=True, text=True) + + self._reload_macos_rules() + + self.logger.info("macOS firewall initialized successfully") + except Exception as e: + self.logger.error(f"Failed to initialize macOS firewall: {e}") + + def _reload_macos_rules(self): + """Reload pfctl rules to ensure blocking rule is active""" + try: + + pf_conf_content = """# Simple Firewall - Dynamic IP Blocking Rules +# This file is managed by Simple Firewall +# DO NOT EDIT MANUALLY + +# Table for blocked IPs +table persist + +# Block all traffic from IPs in the blocked_ips table +block drop in quick from to any +block drop out quick from any to + +# Allow all other traffic (pass through) +pass in all +pass out all +""" + temp_dir = Path(tempfile.gettempdir()) + pf_conf_path = temp_dir / 'simple_firewall_pf.conf' + + with open(pf_conf_path, 'w') as f: + f.write(pf_conf_content) + + cmd = ['sudo', 'pfctl', '-f', str(pf_conf_path)] + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + self.logger.debug("pfctl rules loaded successfully") + else: + self.logger.warning(f"pfctl rule loading returned: {result.returncode}") + self.logger.debug(f"pfctl stderr: {result.stderr}") + + except Exception as e: + self.logger.error(f"Failed to reload macOS firewall rules: {e}") + def _block_ip_macos(self, ip: str) -> bool: """Block IP using pfctl on macOS""" - # First, add IP to a table - cmd1 = ['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'add', ip] - result1 = subprocess.run(cmd1, capture_output=True, text=True) - - # Then enable the blocking rule (this might need to be done once) - cmd2 = ['sudo', 'pfctl', '-e'] - result2 = subprocess.run(cmd2, capture_output=True, text=True) - - return result1.returncode == 0 + try: + # First, add IP to a table + cmd = ['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'add', ip] + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + self.logger.debug(f"Successfully added {ip} to blocked_ips table") + return True + else: + + if 'already' in result.stderr.lower() or 'duplicate' in result.stderr.lower(): + self.logger.debug(f"IP {ip} already in blocked_ips table") + return True + else: + self.logger.error(f"Failed to add {ip} to blocked_ips table: {result.stderr}") + return False + except Exception as e: + self.logger.error(f"Exception while blocking IP {ip} on macOS: {e}") + return False def _unblock_ip_macos(self, ip: str) -> bool: """Unblock IP using pfctl on macOS""" - cmd = ['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'delete', ip] - result = subprocess.run(cmd, capture_output=True, text=True) - return result.returncode == 0 + try: + cmd = ['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'delete', ip] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode == 0: + self.logger.debug(f"Successfully removed {ip} from blocked_ips table") + return True + else: + if 'not found' in result.stderr.lower() or 'does not exist' in result.stderr.lower(): + self.logger.debug(f"IP {ip} not found in blocked_ips table (may have been already removed)") + return True + else: + self.logger.warning(f"Failed to remove {ip} from blocked_ips table: {result.stderr}") + return False + except Exception as e: + self.logger.error(f"Exception while unblocking IP {ip} on macOS: {e}") + return False def _block_ip_windows(self, ip: str) -> bool: """Block IP using Windows Firewall (netsh)""" @@ -216,6 +314,16 @@ def cleanup_all_blocks(self) -> List[str]: if self.unblock_ip(ip): cleaned_ips.append(ip) + # macOS-specific: Clean up the entire table on shutdown + if self.platform == 'darwin' and cleaned_ips: + try: + cmd = ['sudo', 'pfctl', '-t', 'blocked_ips', '-T', 'flush'] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode == 0: + self.logger.info("Cleaned up all blocked IPs from pfctl table") + except Exception as e: + self.logger.warning(f"Failed to flush pfctl table: {e}") + return cleaned_ips def get_stats(self) -> Dict[str, int]: