Skip to content

Commit 78628eb

Browse files
Merge pull request #16 from indrasuthar07/mac-support
feat: macos firewall support using pfctl
2 parents 7b81031 + d07c2be commit 78628eb

File tree

2 files changed

+256
-14
lines changed

2 files changed

+256
-14
lines changed

README.md

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,115 @@ This program uses `netsh` to manage firewall rules, which requires administrativ
9797
# Example:
9898
python main.py -i "Ethernet"
9999
```
100+
101+
## 🍎 Running on macOS
102+
103+
This program uses `pfctl` (Packet Filter Control) to manage firewall rules, which requires administrative privileges.
104+
105+
### Prerequisites
106+
107+
- **macOS 10.5+**
108+
- **Administrator privileges**
109+
- **Python 3.6+**
110+
111+
### Setup Instructions
112+
113+
1. **Open Terminal:** Open the Terminal application (found in Applications > Utilities)
114+
115+
2. **Navigate to Project Directory:**
116+
```bash
117+
cd path/to/simple_firewall
118+
```
119+
120+
3. **(Optional) Create Virtual Environment:**
121+
```bash
122+
python3 -m venv venv
123+
source venv/bin/activate
124+
```
125+
126+
4. **Install Requirements:**
127+
```bash
128+
pip install -r requirements.txt
129+
```
130+
131+
5. **Create Configuration File:**
132+
```bash
133+
python3 main.py --create-config
134+
```
135+
136+
6. **Run the Firewall:**
137+
```bash
138+
sudo python3 main.py
139+
```
140+
141+
Or specify an interface:
142+
```bash
143+
sudo python3 main.py -i en0
144+
```
145+
146+
### macOS Firewall Management
147+
148+
The firewall uses `pfctl` to manage blocking rules:
149+
150+
- **Automatic Setup:** On first run, the firewall automatically:
151+
- Creates a `blocked_ips` table in pfctl
152+
- Enables pfctl if not already enabled
153+
- Loads blocking rules from a temporary configuration file
154+
155+
- **Blocking Mechanism:** IPs are added to a pfctl table, and rules block all traffic from/to those IPs
156+
157+
- **Automatic Cleanup:** When the firewall stops, all blocked IPs are automatically removed
158+
159+
### Managing pfctl Manually
160+
161+
If you need to manually manage the firewall:
162+
163+
```bash
164+
# View currently blocked IPs
165+
sudo pfctl -t blocked_ips -T show
166+
167+
# Remove a specific IP manually
168+
sudo pfctl -t blocked_ips -T delete <IP_ADDRESS>
169+
170+
# Flush all blocked IPs
171+
sudo pfctl -t blocked_ips -T flush
172+
173+
# Check pfctl status
174+
sudo pfctl -s info
175+
176+
# View current rules
177+
sudo pfctl -s rules
178+
```
179+
180+
### Troubleshooting macOS
181+
182+
**Issue: "pfctl: command not found"**
183+
- This shouldn't happen on macOS, but if it does, ensure you're running a supported macOS version
184+
185+
**Issue: "pfctl: Permission denied"**
186+
- Make sure you're running with `sudo`
187+
- Some macOS versions may require additional permissions
188+
189+
**Issue: "pfctl: cannot enable"**
190+
- macOS System Preferences may have the built-in firewall enabled
191+
- You may need to disable it in System Preferences > Security & Privacy > Firewall
192+
- Or allow pfctl to run by granting Full Disk Access in System Preferences
193+
194+
**Issue: Interface not found**
195+
- List available interfaces: `ifconfig` or `networksetup -listallhardwareports`
196+
- Common macOS interfaces: `en0` (Ethernet), `en1` (Wi-Fi), `en2` (USB Ethernet)
197+
198+
**Issue: Blocks not working**
199+
- Check if pfctl is enabled: `sudo pfctl -s info`
200+
- Verify rules are loaded: `sudo pfctl -s rules`
201+
- Check if IPs are in the table: `sudo pfctl -t blocked_ips -T show`
202+
203+
### macOS-Specific Notes
204+
205+
- **System Firewall:** macOS has a built-in firewall in System Preferences. The Simple Firewall works alongside it but uses pfctl directly
206+
- **Interface Names:** macOS uses names like `en0`, `en1`, etc. (not `eth0` like Linux)
207+
- **Root Access:** Always run with `sudo` for firewall operations
208+
- **Temporary Rules:** The firewall creates temporary pf.conf files that are automatically managed
100209
101210
## Usage
102211
@@ -306,8 +415,11 @@ src/
306415
## Requirements
307416
308417
- **Python 3.6+**
309-
- **Root privileges** (required for iptables access)
310-
- **Linux system** (uses iptables for blocking)
418+
- **Root/Administrator privileges** (required for firewall access)
419+
- **Supported Platforms:**
420+
- **Linux:** Uses iptables for blocking
421+
- **macOS:** Uses pfctl (Packet Filter) for blocking
422+
- **Windows:** Uses netsh (Windows Firewall) for blocking
311423
312424
### Python Packages:
313425
- `scapy` - Packet capture and analysis
@@ -389,6 +501,27 @@ sudo iptables -D INPUT -s [IP_ADDRESS] -j DROP
389501
sudo iptables -F INPUT
390502
```
391503
504+
**macOS (pfctl):**
505+
```bash
506+
# View blocked IPs in table
507+
sudo pfctl -t blocked_ips -T show
508+
509+
# Remove specific IP
510+
sudo pfctl -t blocked_ips -T delete [IP_ADDRESS]
511+
512+
# Flush all blocked IPs
513+
sudo pfctl -t blocked_ips -T flush
514+
```
515+
516+
**Windows (netsh):**
517+
```powershell
518+
# List firewall rules
519+
netsh advfirewall firewall show rule name=all
520+
521+
# Delete specific rule (replace IP_ADDRESS with actual IP)
522+
netsh advfirewall firewall delete rule name="SimpleFirewall_Block_[IP_ADDRESS]"
523+
```
524+
392525
## Current Limitations & Known Issues
393526
394527
⚠️ **Platform Limitations:**
@@ -448,6 +581,7 @@ This is an **educational open-source project** designed to help people learn net
448581
- **Performance:** Optimize packet processing and memory usage
449582
- **Testing:** Add unit tests and integration tests
450583
- **Documentation:** Improve code comments and examples
584+
- **IPv6 Support:** Extend blocking to IPv6 addresses
451585
452586
### 📋 **Current Architecture Issues to Fix:**
453587
- Replace hardcoded magic numbers with named constants

src/firewall/blocking.py

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import subprocess
44
import threading
55
import platform
6+
import tempfile
7+
import shutil
68
from datetime import datetime, timedelta
9+
from pathlib import Path
710
from typing import Dict, Set, List
811
from colorama import Fore, Style
912
from 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

Comments
 (0)