feat: routing page — port forwarding tab, live iptables, diagnostics, firewall delete

Backend:
- routing_manager.remove_firewall_rule(): remove stored rule + iptables -D
- routing_manager.get_live_iptables(): dump filter/nat tables from cell-wireguard
- DELETE /api/routing/firewall/<rule_id> endpoint (was missing)
- GET /api/routing/live-iptables endpoint

Frontend Routing.jsx — 7 tabs:
- Overview: proper routing table with destination/gateway/interface columns
- Port Forwarding: clean DNAT form (protocol, ext port → internal IP:port)
- NAT Rules: MASQUERADE/SNAT only, cleaner layout
- Peer Routes: IP route entries through VPN peers
- Firewall: custom rules with working delete button
- Live iptables: read-only terminal view of actual running rules in cell-wireguard
- Diagnostics: ping + traceroute test from server with output display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 01:14:49 -04:00
parent 84d33aa88c
commit 901094f60a
4 changed files with 809 additions and 827 deletions
+43
View File
@@ -485,6 +485,49 @@ class RoutingManager(BaseServiceManager):
logger.error(f"Failed to get routing logs: {e}")
return {'error': str(e)}
def remove_firewall_rule(self, rule_id: str) -> bool:
"""Remove a stored firewall rule and delete it from iptables."""
try:
rules = self._load_rules()
rule = next((r for r in rules['firewall_rules'] if r['id'] == rule_id), None)
if not rule:
return False
rules['firewall_rules'] = [r for r in rules['firewall_rules'] if r['id'] != rule_id]
self._save_rules(rules)
try:
cmd = ['iptables', '-D', rule['rule_type'],
'-s', rule['source'], '-d', rule['destination']]
if rule.get('protocol') and rule['protocol'] != 'ALL':
cmd += ['-p', rule['protocol'].lower()]
if rule.get('port'):
cmd += ['--dport', str(rule['port'])]
if rule.get('port_range'):
cmd += ['--dport', rule['port_range'].replace('-', ':')]
cmd += ['-j', rule['action']]
subprocess.run(cmd, capture_output=True, timeout=10)
except Exception as e:
logger.warning(f"iptables -D failed (rule may already be gone): {e}")
logger.info(f"Removed firewall rule {rule_id}")
return True
except Exception as e:
logger.error(f"Failed to remove firewall rule: {e}")
return False
def get_live_iptables(self) -> dict:
"""Return live iptables rules from the WireGuard container."""
out = {}
for table in ('filter', 'nat'):
try:
r = subprocess.run(
['docker', 'exec', 'cell-wireguard',
'iptables', '-t', table, '-L', '-n', '-v', '--line-numbers'],
capture_output=True, text=True, timeout=10
)
out[table] = r.stdout if r.returncode == 0 else r.stderr
except Exception as e:
out[table] = str(e)
return out
def get_nat_rules(self):
"""Return all NAT rules."""
rules = self._load_rules()