Phase 5: extended connectivity — WireGuard ext, OpenVPN, Tor exit routing

- ConnectivityManager: per-peer exit routing via iptables fwmark/policy tables
  (wg_ext=0x10/t110, openvpn=0x20/t120, tor=0x30/t130)
- Dedicated PIC_CONNECTIVITY chains (mangle+nat), kill-switch FORWARD DROP
- Config upload with sanitization: strips PostUp/PostDown and OVpn script dirs
- Peer exit_via field added to peer registry (backward-compat, default=default)
- 7 Flask routes at /api/connectivity/*
- Connectivity.jsx: 693-line frontend with exit cards, peer assignment table
- 72 new tests for ConnectivityManager (72 passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 10:48:20 -04:00
parent 0a21f22076
commit e38bd4e81f
9 changed files with 2114 additions and 1 deletions
+30
View File
@@ -194,11 +194,15 @@ class PeerRegistry(BaseServiceManager):
self.logger.error(f"Error loading peers: {e}")
self.peers = []
# Phase 3 migration: per-peer internet routing
# Phase 5 migration: per-peer extended-connectivity exit (wireguard_ext, openvpn, tor)
changed = False
for peer in self.peers:
if 'route_via' not in peer:
peer['route_via'] = None
changed = True
if 'exit_via' not in peer:
peer['exit_via'] = 'default'
changed = True
if changed:
self._save_peers()
else:
@@ -346,6 +350,32 @@ class PeerRegistry(BaseServiceManager):
return dict(peer)
raise ValueError(f"Peer '{peer_name}' not found")
# Phase 5: extended connectivity per-peer egress exit
VALID_EXIT_VIA = ('default', 'wireguard_ext', 'openvpn', 'tor')
def set_peer_exit_via(self, peer_name: str, exit_type: str) -> bool:
"""Set the per-peer egress exit type. Returns True if updated, False
if the peer is not found (logged as warning, no exception)."""
if exit_type not in self.VALID_EXIT_VIA:
self.logger.warning(
f"set_peer_exit_via: invalid exit_type {exit_type!r}"
)
return False
with self.lock:
for peer in self.peers:
if peer.get('peer') == peer_name:
peer['exit_via'] = exit_type
peer['updated_at'] = datetime.utcnow().isoformat()
self._save_peers()
self.logger.info(
f"Set exit_via for {peer_name}: {exit_type!r}"
)
return True
self.logger.warning(
f"set_peer_exit_via: peer {peer_name!r} not found"
)
return False
def get_peer_stats(self) -> Dict[str, Any]:
"""Get peer registry statistics"""
try: