feat: connectivity redesign phase 2 — instance-aware routing + reference connections by id
Unit Tests / test (push) Successful in 12m6s
Unit Tests / test (push) Successful in 12m6s
apply_routes now iterates over connection instances rather than types:
each instance gets its own fwmark, routing table, interface, and
redirect_port via _routing_connections / _resolve_peer_connection /
_apply_connection_for_src; kill-switch is enforced per iface-instance.
Old per-type MARKS/TABLES constants are kept only as migration scaffolding.
peer_registry: exit_via is now stored as a connection id (or 'default');
_migrate_exit_via_to_connection_id runs on _load_peers to upgrade legacy
type-string values; set_peer_exit_via validates against known connection
ids; VALID_EXIT_VIA removed; config_manager wired in from managers.py.
egress_manager: egress_overrides keyed by service_id → connection_id;
local MARKS/TABLES/EXIT_TYPES/_REDIRECT_PORTS/_add_tor_redirect removed;
(mark, table, redirect_port) resolved at apply-time via
connectivity_manager.get_connection; manifest egress.allowed still
enforced by connection type.
api/app.py + api.js: PUT peer/service exit endpoints accept {connection_id};
back-compat shim resolves a legacy type string to its single active instance.
Tests extended: two same-type instances produce distinct marks/tables/ports;
peer exit_via and egress override id migrations round-trip correctly;
single-instance behaviour is equivalent to the old type-keyed path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+96
-10
@@ -17,11 +17,17 @@ logger = logging.getLogger(__name__)
|
||||
class PeerRegistry(BaseServiceManager):
|
||||
"""Manages peer registration and management"""
|
||||
|
||||
def __init__(self, data_dir: str = '/app/data', config_dir: str = '/app/config'):
|
||||
def __init__(self, data_dir: str = '/app/data', config_dir: str = '/app/config',
|
||||
config_manager=None):
|
||||
super().__init__('peer_registry', data_dir, config_dir)
|
||||
self.lock = RLock()
|
||||
self.peers = []
|
||||
self.peers_file = os.path.join(data_dir, 'peers.json')
|
||||
# config_manager is used to resolve/validate connection ids for the
|
||||
# per-peer exit (exit_via). It may be wired after construction (the
|
||||
# singletons in managers.py are built in dependency order), so the
|
||||
# exit_via→connection-id migration also runs lazily, idempotently.
|
||||
self.config_manager = config_manager
|
||||
self._load_peers()
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
@@ -205,6 +211,11 @@ class PeerRegistry(BaseServiceManager):
|
||||
changed = True
|
||||
if changed:
|
||||
self._save_peers()
|
||||
# Phase 2 (connectivity v2): exit_via is now a connection id (or
|
||||
# 'default'). Rewrite any legacy per-type exit_via to the id of
|
||||
# the single migrated connection instance of that type. Runs
|
||||
# lazily if config_manager is not yet wired.
|
||||
self._migrate_exit_via_to_connection_id()
|
||||
else:
|
||||
self.peers = []
|
||||
self.logger.info("No peers file found, starting with empty registry")
|
||||
@@ -350,26 +361,101 @@ 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',
|
||||
'sshuttle', 'proxy')
|
||||
# Connectivity v2: legacy per-type exit values. A peer's exit_via is now a
|
||||
# connection id (or 'default'); these strings are accepted only as a
|
||||
# one-release back-compat shim — resolved to the single migrated instance
|
||||
# of that type via config_manager.list_connections().
|
||||
_LEGACY_EXIT_TYPES = ('wireguard_ext', 'openvpn', 'tor', 'sshuttle', 'proxy')
|
||||
|
||||
def _connections(self) -> List[Dict[str, Any]]:
|
||||
"""Return the v2 connection records, or [] when unavailable."""
|
||||
if self.config_manager is None:
|
||||
return []
|
||||
try:
|
||||
conns = self.config_manager.list_connections()
|
||||
except Exception as e:
|
||||
self.logger.warning(f"peer_registry: list_connections failed: {e}")
|
||||
return []
|
||||
return conns if isinstance(conns, list) else []
|
||||
|
||||
def _resolve_exit_via(self, value: str) -> Optional[str]:
|
||||
"""Resolve an exit_via value to a valid connection id or 'default'.
|
||||
|
||||
Accepts 'default', a real connection id, or — as a back-compat shim —
|
||||
a legacy type string (resolved to the single instance of that type).
|
||||
Returns None when the value cannot be resolved to anything valid.
|
||||
"""
|
||||
if value == 'default':
|
||||
return 'default'
|
||||
conns = self._connections()
|
||||
for c in conns:
|
||||
if c.get('id') == value:
|
||||
return value
|
||||
if value in self._LEGACY_EXIT_TYPES:
|
||||
matches = [c for c in conns if c.get('type') == value]
|
||||
if len(matches) == 1:
|
||||
return matches[0].get('id')
|
||||
return None
|
||||
|
||||
def _migrate_exit_via_to_connection_id(self) -> bool:
|
||||
"""Rewrite legacy per-type exit_via values to migrated connection ids.
|
||||
|
||||
Idempotent: ids and 'default' are left untouched. Legacy type strings
|
||||
are mapped to the single instance of that type; if no instance exists
|
||||
the peer falls back to 'default'. Returns True if anything changed.
|
||||
Runs only when config_manager (and its v2 connections) are available.
|
||||
"""
|
||||
if self.config_manager is None:
|
||||
return False
|
||||
conns = self._connections()
|
||||
valid_ids = {c.get('id') for c in conns}
|
||||
by_type: Dict[str, List[str]] = {}
|
||||
for c in conns:
|
||||
by_type.setdefault(c.get('type'), []).append(c.get('id'))
|
||||
|
||||
changed = False
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
exit_via = peer.get('exit_via', 'default')
|
||||
if exit_via == 'default' or exit_via in valid_ids:
|
||||
continue
|
||||
new_value = 'default'
|
||||
if exit_via in self._LEGACY_EXIT_TYPES:
|
||||
ids = by_type.get(exit_via, [])
|
||||
if len(ids) == 1:
|
||||
new_value = ids[0]
|
||||
peer['exit_via'] = new_value
|
||||
changed = True
|
||||
self.logger.info(
|
||||
f"peer_registry: migrated exit_via {exit_via!r} → "
|
||||
f"{new_value!r} for {peer.get('peer')!r}"
|
||||
)
|
||||
if changed:
|
||||
self._save_peers()
|
||||
return changed
|
||||
|
||||
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:
|
||||
"""Set the per-peer egress connection id. Returns True if updated, False
|
||||
if the peer is not found or the id is invalid (logged, no exception).
|
||||
|
||||
`exit_type` must be a real connection id or 'default'. A legacy type
|
||||
string is accepted as a back-compat shim and resolved to the single
|
||||
instance of that type.
|
||||
"""
|
||||
resolved = self._resolve_exit_via(exit_type)
|
||||
if resolved is None:
|
||||
self.logger.warning(
|
||||
f"set_peer_exit_via: invalid exit_type {exit_type!r}"
|
||||
f"set_peer_exit_via: invalid connection id {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['exit_via'] = resolved
|
||||
peer['updated_at'] = datetime.utcnow().isoformat()
|
||||
self._save_peers()
|
||||
self.logger.info(
|
||||
f"Set exit_via for {peer_name}: {exit_type!r}"
|
||||
f"Set exit_via for {peer_name}: {resolved!r}"
|
||||
)
|
||||
return True
|
||||
self.logger.warning(
|
||||
|
||||
Reference in New Issue
Block a user