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
+33
View File
@@ -40,6 +40,9 @@ class ConfigManager:
# Ensure _identity key always exists
if '_identity' not in self.configs:
self.configs['_identity'] = {}
# Phase 5: ensure connectivity section exists with empty defaults.
if 'connectivity' not in self.configs:
self.configs['connectivity'] = {'exits': {}, 'peer_exit_map': {}}
if not self.config_file.exists():
self._save_all_configs()
@@ -108,6 +111,14 @@ class ConfigManager:
'ca_configured': bool,
'fernet_configured': bool
}
},
'connectivity': {
'required': [],
'optional': ['exits', 'peer_exit_map'],
'types': {
'exits': dict,
'peer_exit_map': dict,
}
}
}
@@ -488,6 +499,28 @@ class ConfigManager:
ident.setdefault('service_ips', {}).pop(service_id, None)
self._save_all_configs()
# Phase 5 — Extended connectivity configuration helpers
def get_connectivity_config(self) -> Dict[str, Any]:
"""Return the full connectivity config (exits + peer_exit_map)."""
cfg = self.configs.get('connectivity')
if not isinstance(cfg, dict):
cfg = {'exits': {}, 'peer_exit_map': {}}
self.configs['connectivity'] = cfg
cfg.setdefault('exits', {})
cfg.setdefault('peer_exit_map', {})
return dict(cfg)
def set_connectivity_field(self, field: str, value: Any) -> bool:
"""Set a single field within the connectivity config and persist."""
cfg = self.configs.setdefault('connectivity', {'exits': {}, 'peer_exit_map': {}})
cfg[field] = value
try:
self._save_all_configs()
return True
except Exception as e:
logger.error(f"set_connectivity_field({field}): {e}")
return False
def get_all_configs(self) -> Dict[str, Dict]:
"""Get all service configurations"""
return self.configs.copy()