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:
@@ -37,6 +37,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()
|
||||
@@ -265,13 +266,26 @@ class TestApplyRoutesProxy(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_proxy_peer_gets_redirect_to_12345(self):
|
||||
@staticmethod
|
||||
def _proxy_conn(redirect_port=9100, mark=0x1000, table=1000):
|
||||
return {'id': 'conn_proxy', 'type': 'proxy', '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_proxy_peer_gets_redirect_to_instance_port(self):
|
||||
conn = self._proxy_conn(redirect_port=9100)
|
||||
pr = MagicMock()
|
||||
pr.list_peers.return_value = [
|
||||
{'peer': 'bob', 'exit_via': 'proxy'},
|
||||
]
|
||||
pr.list_peers.return_value = [{'peer': 'bob', 'exit_via': 'conn_proxy'}]
|
||||
pr.get_peer.return_value = {'peer': 'bob', 'ip': '172.20.0.60/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()
|
||||
@@ -281,16 +295,16 @@ class TestApplyRoutesProxy(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(len(redirect_calls), 1)
|
||||
args = redirect_calls[0].args[0]
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '12345')
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '9100')
|
||||
self.assertIn('172.20.0.60', args)
|
||||
|
||||
def test_proxy_peer_gets_mark_0x50(self):
|
||||
def test_proxy_peer_gets_instance_mark(self):
|
||||
conn = self._proxy_conn(mark=0x1020)
|
||||
pr = MagicMock()
|
||||
pr.list_peers.return_value = [
|
||||
{'peer': 'bob', 'exit_via': 'proxy'},
|
||||
]
|
||||
pr.list_peers.return_value = [{'peer': 'bob', 'exit_via': 'conn_proxy'}]
|
||||
pr.get_peer.return_value = {'peer': 'bob', 'ip': '172.20.0.60/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()
|
||||
@@ -300,29 +314,32 @@ class TestApplyRoutesProxy(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(len(mark_calls), 1)
|
||||
args = mark_calls[0].args[0]
|
||||
self.assertEqual(args[args.index('--set-mark') + 1], hex(0x50))
|
||||
self.assertEqual(args[args.index('--set-mark') + 1], hex(0x1020))
|
||||
|
||||
def test_ip_rule_added_for_proxy_table_150(self):
|
||||
mgr = _make_manager(tmp_dir=self.tmp)
|
||||
def test_ip_rule_added_for_instance_table(self):
|
||||
conn = self._proxy_conn(mark=0x1030, table=1234)
|
||||
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(0x50) in c.args[0]
|
||||
and hex(0x1030) in c.args[0]
|
||||
]
|
||||
self.assertEqual(len(rule_adds), 1)
|
||||
self.assertIn('150', rule_adds[0].args[0])
|
||||
self.assertIn('1234', rule_adds[0].args[0])
|
||||
|
||||
def test_tor_redirect_still_uses_9040(self):
|
||||
"""Regression: tor redirect must be unaffected by the new exits."""
|
||||
def test_tor_redirect_uses_instance_port(self):
|
||||
"""A tor connection instance REDIRECTs to its own allocated port."""
|
||||
conn = {'id': 'conn_tor', 'type': 'tor', 'enabled': True,
|
||||
'mark': 0x1000, 'table': 1000, 'iface': None,
|
||||
'redirect_port': 9100}
|
||||
pr = MagicMock()
|
||||
pr.list_peers.return_value = [
|
||||
{'peer': 'carol', 'exit_via': 'tor'},
|
||||
]
|
||||
pr.list_peers.return_value = [{'peer': 'carol', 'exit_via': 'conn_tor'}]
|
||||
pr.get_peer.return_value = {'peer': 'carol', 'ip': '172.20.0.70/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()
|
||||
@@ -332,7 +349,7 @@ class TestApplyRoutesProxy(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(len(redirect_calls), 1)
|
||||
args = redirect_calls[0].args[0]
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '9040')
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '9100')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -340,39 +357,32 @@ class TestApplyRoutesProxy(unittest.TestCase):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEgressManagerMirror(unittest.TestCase):
|
||||
"""Egress now resolves a service's (mark, table, redirect_port) from the
|
||||
connection instance it routes through — no per-type MARKS/TABLES tables."""
|
||||
|
||||
def test_exit_types_include_sshuttle_and_proxy(self):
|
||||
self.assertIn('sshuttle', em_module.EXIT_TYPES)
|
||||
self.assertIn('proxy', em_module.EXIT_TYPES)
|
||||
|
||||
def test_marks_do_not_collide_with_connectivity(self):
|
||||
self.assertEqual(em_module.MARKS['sshuttle'], 0x140)
|
||||
self.assertEqual(em_module.MARKS['proxy'], 0x150)
|
||||
self.assertNotIn(em_module.MARKS['sshuttle'],
|
||||
ConnectivityManager.MARKS.values())
|
||||
self.assertNotIn(em_module.MARKS['proxy'],
|
||||
ConnectivityManager.MARKS.values())
|
||||
|
||||
def test_tables(self):
|
||||
self.assertEqual(em_module.TABLES['sshuttle'], 240)
|
||||
self.assertEqual(em_module.TABLES['proxy'], 250)
|
||||
|
||||
def _make_egress(self, exit_via):
|
||||
def _make_egress(self, connection):
|
||||
config_manager = MagicMock()
|
||||
manifest = {
|
||||
'id': 'svc',
|
||||
'container_name': 'cell-svc',
|
||||
'has_egress': True,
|
||||
'egress': {'default': exit_via, 'allowed': list(em_module.EXIT_TYPES)},
|
||||
'egress': {'default': connection['id'],
|
||||
'allowed': [connection['type']]},
|
||||
}
|
||||
config_manager.get_installed_services.return_value = {
|
||||
'svc': {'manifest': manifest},
|
||||
}
|
||||
config_manager.configs = {'egress_overrides': {}}
|
||||
config_manager.list_connections.return_value = [connection]
|
||||
config_manager.get_connection.side_effect = \
|
||||
lambda cid: connection if cid == connection['id'] else None
|
||||
return em_module.EgressManager(config_manager=config_manager)
|
||||
|
||||
def test_apply_service_sshuttle_redirects_to_12300(self):
|
||||
em = self._make_egress('sshuttle')
|
||||
def test_apply_service_sshuttle_redirects_to_instance_port(self):
|
||||
conn = {'id': 'conn_ssh', 'type': 'sshuttle', 'enabled': True,
|
||||
'mark': 0x1000, 'table': 1000, 'iface': None,
|
||||
'redirect_port': 9100}
|
||||
em = self._make_egress(conn)
|
||||
with patch.object(em_module, 'subprocess') as mock_sp:
|
||||
mock_sp.run.return_value = MagicMock(
|
||||
returncode=0, stdout='172.21.0.5', stderr='')
|
||||
@@ -384,10 +394,13 @@ class TestEgressManagerMirror(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(len(redirect_calls), 1)
|
||||
args = redirect_calls[0].args[0]
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '12300')
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '9100')
|
||||
|
||||
def test_apply_service_proxy_redirects_to_12345(self):
|
||||
em = self._make_egress('proxy')
|
||||
def test_apply_service_proxy_redirects_to_instance_port(self):
|
||||
conn = {'id': 'conn_px', 'type': 'proxy', 'enabled': True,
|
||||
'mark': 0x1010, 'table': 1001, 'iface': None,
|
||||
'redirect_port': 9101}
|
||||
em = self._make_egress(conn)
|
||||
with patch.object(em_module, 'subprocess') as mock_sp:
|
||||
mock_sp.run.return_value = MagicMock(
|
||||
returncode=0, stdout='172.21.0.5', stderr='')
|
||||
@@ -399,7 +412,24 @@ class TestEgressManagerMirror(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(len(redirect_calls), 1)
|
||||
args = redirect_calls[0].args[0]
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '12345')
|
||||
self.assertEqual(args[args.index('--to-ports') + 1], '9101')
|
||||
|
||||
def test_apply_service_uses_connection_mark(self):
|
||||
conn = {'id': 'conn_px', 'type': 'proxy', 'enabled': True,
|
||||
'mark': 0x1010, 'table': 1001, 'iface': None,
|
||||
'redirect_port': 9101}
|
||||
em = self._make_egress(conn)
|
||||
with patch.object(em_module, 'subprocess') as mock_sp:
|
||||
mock_sp.run.return_value = MagicMock(
|
||||
returncode=0, stdout='172.21.0.5', stderr='')
|
||||
em.apply_service('svc')
|
||||
mark_calls = [
|
||||
c for c in mock_sp.run.call_args_list
|
||||
if 'MARK' in c.args[0] and '--set-mark' in c.args[0]
|
||||
]
|
||||
self.assertGreater(len(mark_calls), 0)
|
||||
args = mark_calls[0].args[0]
|
||||
self.assertEqual(args[args.index('--set-mark') + 1], hex(0x1010))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user