feat: connectivity redesign phase 2 — instance-aware routing + reference connections by id
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:
2026-06-10 17:35:28 -04:00
parent 5b9d20eeac
commit 89aed4efe0
12 changed files with 993 additions and 375 deletions
+54 -21
View File
@@ -47,6 +47,7 @@ def _make_manager(tmp_dir=None, peer_registry=_SENTINEL, config_manager=None,
'exits': {}, 'peer_exit_map': {},
}
config_manager.get_installed_services.return_value = {}
config_manager.list_connections.return_value = []
if peer_registry is _SENTINEL:
peer_registry = MagicMock()
@@ -344,13 +345,26 @@ class TestApplyRoutesSshuttle(unittest.TestCase):
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def test_sshuttle_peer_gets_redirect_to_12300(self):
@staticmethod
def _ssh_conn(mark=0x1000, table=1000, redirect_port=9100):
return {'id': 'conn_ssh', 'type': 'sshuttle', 'enabled': True,
'mark': mark, 'table': table, 'iface': None,
'redirect_port': redirect_port}
def _cm(self, connections):
cm = MagicMock()
cm.get_identity.return_value = {'cell_name': 't', 'ip_range': '172.20.0.0/16'}
cm.list_connections.return_value = connections
cm.get_installed_services.return_value = {}
return cm
def test_sshuttle_peer_gets_redirect_to_instance_port(self):
conn = self._ssh_conn(redirect_port=9100)
pr = MagicMock()
pr.list_peers.return_value = [
{'peer': 'alice', 'exit_via': 'sshuttle'},
]
pr.list_peers.return_value = [{'peer': 'alice', 'exit_via': 'conn_ssh'}]
pr.get_peer.return_value = {'peer': 'alice', 'ip': '172.20.0.50/32'}
mgr = _make_manager(tmp_dir=self.tmp, peer_registry=pr)
mgr = _make_manager(tmp_dir=self.tmp, peer_registry=pr,
config_manager=self._cm([conn]))
with patch.object(cm_module, 'subprocess') as mock_sp:
mock_sp.run.return_value = _mock_subprocess_ok()
mgr.apply_routes()
@@ -361,16 +375,16 @@ class TestApplyRoutesSshuttle(unittest.TestCase):
self.assertEqual(len(redirect_calls), 1)
args = redirect_calls[0].args[0]
self.assertIn('--to-ports', args)
self.assertEqual(args[args.index('--to-ports') + 1], '12300')
self.assertEqual(args[args.index('--to-ports') + 1], '9100')
self.assertIn('172.20.0.50', args)
def test_sshuttle_peer_gets_mark_0x40(self):
def test_sshuttle_peer_gets_instance_mark(self):
conn = self._ssh_conn(mark=0x1040)
pr = MagicMock()
pr.list_peers.return_value = [
{'peer': 'alice', 'exit_via': 'sshuttle'},
]
pr.list_peers.return_value = [{'peer': 'alice', 'exit_via': 'conn_ssh'}]
pr.get_peer.return_value = {'peer': 'alice', 'ip': '172.20.0.50/32'}
mgr = _make_manager(tmp_dir=self.tmp, peer_registry=pr)
mgr = _make_manager(tmp_dir=self.tmp, peer_registry=pr,
config_manager=self._cm([conn]))
with patch.object(cm_module, 'subprocess') as mock_sp:
mock_sp.run.return_value = _mock_subprocess_ok()
mgr.apply_routes()
@@ -380,20 +394,21 @@ class TestApplyRoutesSshuttle(unittest.TestCase):
]
self.assertEqual(len(mark_calls), 1)
args = mark_calls[0].args[0]
self.assertEqual(args[args.index('--set-mark') + 1], hex(0x40))
self.assertEqual(args[args.index('--set-mark') + 1], hex(0x1040))
def test_ip_rule_added_for_sshuttle_table_140(self):
mgr = _make_manager(tmp_dir=self.tmp)
def test_ip_rule_added_for_instance_table(self):
conn = self._ssh_conn(mark=0x1040, table=1399)
mgr = _make_manager(tmp_dir=self.tmp, config_manager=self._cm([conn]))
with patch.object(cm_module, 'subprocess') as mock_sp:
mock_sp.run.return_value = MagicMock(returncode=1, stdout='', stderr='')
mgr.apply_routes()
rule_adds = [
c for c in mock_sp.run.call_args_list
if 'rule' in c.args[0] and 'add' in c.args[0]
and hex(0x40) in c.args[0]
and hex(0x1040) in c.args[0]
]
self.assertEqual(len(rule_adds), 1)
self.assertIn('140', rule_adds[0].args[0])
self.assertIn('1399', rule_adds[0].args[0])
def test_no_killswitch_for_sshuttle(self):
"""sshuttle has no exit iface — _add_killswitch must skip it."""
@@ -481,19 +496,37 @@ class TestSetPeerExitSshuttle(unittest.TestCase):
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def test_sshuttle_is_a_valid_exit_type(self):
def test_legacy_sshuttle_type_resolves_to_instance(self):
"""Back-compat shim: setting exit to the legacy 'sshuttle' type resolves
to the single sshuttle connection instance."""
conn = {'id': 'conn_ssh', 'type': 'sshuttle'}
cm = MagicMock()
cm.get_identity.return_value = {'ip_range': '172.20.0.0/16'}
cm.list_connections.return_value = [conn]
cm.get_installed_services.return_value = {}
pr = MagicMock()
pr.set_peer_exit_via.return_value = True
pr.list_peers.return_value = []
mgr = _make_manager(tmp_dir=self.tmp, peer_registry=pr)
pr.get_peer.return_value = {'peer': 'alice', 'exit_via': 'conn_ssh'}
def _set(name, value):
# mimic the real registry shim resolution
return value in ('default', 'conn_ssh', 'sshuttle')
pr.set_peer_exit_via.side_effect = _set
mgr = _make_manager(tmp_dir=self.tmp, peer_registry=pr, config_manager=cm)
with patch.object(cm_module, 'subprocess') as mock_sp:
mock_sp.run.return_value = _mock_subprocess_ok()
result = mgr.set_peer_exit('alice', 'sshuttle')
self.assertTrue(result['ok'])
def test_peer_registry_accepts_sshuttle(self):
def test_peer_registry_accepts_sshuttle_legacy_type(self):
"""The peer registry resolves a legacy 'sshuttle' type to its instance id."""
from peer_registry import PeerRegistry
self.assertIn('sshuttle', PeerRegistry.VALID_EXIT_VIA)
cm = MagicMock()
cm.list_connections.return_value = [{'id': 'conn_ssh', 'type': 'sshuttle'}]
reg = PeerRegistry(data_dir=self.tmp, config_dir=self.tmp, config_manager=cm)
reg.add_peer({'peer': 'alice', 'ip': '10.0.0.5'})
self.assertTrue(reg.set_peer_exit_via('alice', 'sshuttle'))
self.assertEqual(reg.get_peer('alice')['exit_via'], 'conn_ssh')
# ---------------------------------------------------------------------------