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:
+71
-15
@@ -5,6 +5,7 @@ import os
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Add api directory to path
|
||||
api_dir = Path(__file__).parent.parent / 'api'
|
||||
@@ -107,35 +108,90 @@ class TestPeerRegistry(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
self.registry.set_route_via('nobody', 'exit-cell')
|
||||
|
||||
def test_set_peer_exit_via_valid(self):
|
||||
def _connectivity_cm(self, connections):
|
||||
"""A mock config_manager exposing v2 connection records."""
|
||||
cm = MagicMock()
|
||||
cm.list_connections.return_value = connections
|
||||
return cm
|
||||
|
||||
def test_set_peer_exit_via_valid_connection_id(self):
|
||||
conns = [{'id': 'conn_wg', 'type': 'wireguard_ext'}]
|
||||
self.registry.config_manager = self._connectivity_cm(conns)
|
||||
self.registry.add_peer({'peer': 'alice', 'ip': '10.0.0.5'})
|
||||
result = self.registry.set_peer_exit_via('alice', 'wireguard_ext')
|
||||
result = self.registry.set_peer_exit_via('alice', 'conn_wg')
|
||||
self.assertTrue(result)
|
||||
peer = self.registry.get_peer('alice')
|
||||
self.assertEqual(peer['exit_via'], 'wireguard_ext')
|
||||
self.assertEqual(self.registry.get_peer('alice')['exit_via'], 'conn_wg')
|
||||
|
||||
def test_set_peer_exit_via_all_valid_types(self):
|
||||
def test_set_peer_exit_via_default_always_valid(self):
|
||||
self.registry.config_manager = self._connectivity_cm([])
|
||||
self.registry.add_peer({'peer': 'alice', 'ip': '10.0.0.5'})
|
||||
for exit_type in ('default', 'wireguard_ext', 'openvpn', 'tor'):
|
||||
result = self.registry.set_peer_exit_via('alice', exit_type)
|
||||
self.assertTrue(result)
|
||||
peer = self.registry.get_peer('alice')
|
||||
self.assertEqual(peer['exit_via'], exit_type)
|
||||
result = self.registry.set_peer_exit_via('alice', 'default')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(self.registry.get_peer('alice')['exit_via'], 'default')
|
||||
|
||||
def test_set_peer_exit_via_invalid_type_returns_false(self):
|
||||
def test_set_peer_exit_via_legacy_type_resolves_to_instance(self):
|
||||
"""Back-compat shim: a legacy type resolves to the one instance of it."""
|
||||
conns = [{'id': 'conn_tor', 'type': 'tor'}]
|
||||
self.registry.config_manager = self._connectivity_cm(conns)
|
||||
self.registry.add_peer({'peer': 'alice', 'ip': '10.0.0.5'})
|
||||
result = self.registry.set_peer_exit_via('alice', 'invalid_exit')
|
||||
result = self.registry.set_peer_exit_via('alice', 'tor')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(self.registry.get_peer('alice')['exit_via'], 'conn_tor')
|
||||
|
||||
def test_set_peer_exit_via_unknown_id_returns_false(self):
|
||||
self.registry.config_manager = self._connectivity_cm([])
|
||||
self.registry.add_peer({'peer': 'alice', 'ip': '10.0.0.5'})
|
||||
result = self.registry.set_peer_exit_via('alice', 'conn_ghost')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_set_peer_exit_via_nonexistent_peer_returns_false(self):
|
||||
self.registry.config_manager = self._connectivity_cm([])
|
||||
result = self.registry.set_peer_exit_via('nobody', 'default')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_set_peer_exit_via_persists(self):
|
||||
conns = [{'id': 'conn_tor', 'type': 'tor'}]
|
||||
cm = self._connectivity_cm(conns)
|
||||
self.registry.config_manager = cm
|
||||
self.registry.add_peer({'peer': 'alice', 'ip': '10.0.0.5'})
|
||||
self.registry.set_peer_exit_via('alice', 'tor')
|
||||
reloaded = PeerRegistry(data_dir=self.test_dir, config_dir=self.test_dir)
|
||||
self.assertEqual(reloaded.get_peer('alice')['exit_via'], 'tor')
|
||||
self.registry.set_peer_exit_via('alice', 'conn_tor')
|
||||
reloaded = PeerRegistry(data_dir=self.test_dir, config_dir=self.test_dir,
|
||||
config_manager=cm)
|
||||
self.assertEqual(reloaded.get_peer('alice')['exit_via'], 'conn_tor')
|
||||
|
||||
def test_exit_via_migration_legacy_type_to_id(self):
|
||||
"""On load, a legacy per-type exit_via becomes the migrated instance id."""
|
||||
peers_file = os.path.join(self.test_dir, 'peers.json')
|
||||
with open(peers_file, 'w') as f:
|
||||
json.dump([{'peer': 'alice', 'ip': '10.0.0.5',
|
||||
'exit_via': 'wireguard_ext'}], f)
|
||||
cm = self._connectivity_cm([{'id': 'conn_wg', 'type': 'wireguard_ext'}])
|
||||
reg = PeerRegistry(data_dir=self.test_dir, config_dir=self.test_dir,
|
||||
config_manager=cm)
|
||||
self.assertEqual(reg.get_peer('alice')['exit_via'], 'conn_wg')
|
||||
|
||||
def test_exit_via_migration_unknown_type_to_default(self):
|
||||
"""A legacy type with no migrated instance falls back to 'default'."""
|
||||
peers_file = os.path.join(self.test_dir, 'peers.json')
|
||||
with open(peers_file, 'w') as f:
|
||||
json.dump([{'peer': 'alice', 'ip': '10.0.0.5',
|
||||
'exit_via': 'openvpn'}], f)
|
||||
cm = self._connectivity_cm([]) # no instances
|
||||
reg = PeerRegistry(data_dir=self.test_dir, config_dir=self.test_dir,
|
||||
config_manager=cm)
|
||||
self.assertEqual(reg.get_peer('alice')['exit_via'], 'default')
|
||||
|
||||
def test_exit_via_migration_id_is_idempotent(self):
|
||||
"""An already-migrated id is left untouched and not re-migrated."""
|
||||
peers_file = os.path.join(self.test_dir, 'peers.json')
|
||||
with open(peers_file, 'w') as f:
|
||||
json.dump([{'peer': 'alice', 'ip': '10.0.0.5',
|
||||
'exit_via': 'conn_wg'}], f)
|
||||
cm = self._connectivity_cm([{'id': 'conn_wg', 'type': 'wireguard_ext'}])
|
||||
reg = PeerRegistry(data_dir=self.test_dir, config_dir=self.test_dir,
|
||||
config_manager=cm)
|
||||
self.assertEqual(reg.get_peer('alice')['exit_via'], 'conn_wg')
|
||||
self.assertFalse(reg._migrate_exit_via_to_connection_id())
|
||||
|
||||
def test_update_peer_updates_arbitrary_fields(self):
|
||||
self.registry.add_peer({'peer': 'alice', 'ip': '10.0.0.5'})
|
||||
|
||||
Reference in New Issue
Block a user