fix: allow reply traffic from connected cells through FORWARD chain
apply_cell_rules drops all traffic from a cell's subnet except specific service ports. This also drops ICMP replies and TCP ACKs for connections initiated by local peers to the connected cell, breaking cross-cell routing (ping to 10.0.0.1 silently dropped by test's cell DROP rule). Fix: ensure_forward_stateful() inserts a stateful ESTABLISHED,RELATED ACCEPT at the top of FORWARD. Called from apply_cell_rules (every cell add/update) and from _apply_startup_enforcement. Idempotent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -929,5 +929,56 @@ class TestReconcileStale(unittest.TestCase):
|
||||
self.assertIn('10.0.0.8', cleared)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ensure_forward_stateful
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEnsureForwardStateful(unittest.TestCase):
|
||||
"""ensure_forward_stateful must insert ESTABLISHED,RELATED ACCEPT only once."""
|
||||
|
||||
def _make_exec(self, already_present=False):
|
||||
calls = []
|
||||
def fake_wg_exec(args):
|
||||
calls.append(args)
|
||||
r = MagicMock()
|
||||
# -C (check) returns 0 if present, 1 if not
|
||||
if '-C' in args:
|
||||
r.returncode = 0 if already_present else 1
|
||||
else:
|
||||
r.returncode = 0
|
||||
r.stdout = ''
|
||||
return r
|
||||
return calls, fake_wg_exec
|
||||
|
||||
def test_inserts_rule_when_not_present(self):
|
||||
calls, fake = self._make_exec(already_present=False)
|
||||
with patch.object(firewall_manager, '_wg_exec', side_effect=fake):
|
||||
result = firewall_manager.ensure_forward_stateful()
|
||||
self.assertTrue(result)
|
||||
insert_calls = [c for c in calls if '-I' in c]
|
||||
self.assertEqual(len(insert_calls), 1)
|
||||
flat = ' '.join(insert_calls[0])
|
||||
self.assertIn('ESTABLISHED,RELATED', flat)
|
||||
self.assertIn('ACCEPT', flat)
|
||||
|
||||
def test_skips_insert_when_already_present(self):
|
||||
calls, fake = self._make_exec(already_present=True)
|
||||
with patch.object(firewall_manager, '_wg_exec', side_effect=fake):
|
||||
result = firewall_manager.ensure_forward_stateful()
|
||||
self.assertTrue(result)
|
||||
insert_calls = [c for c in calls if '-I' in c]
|
||||
self.assertEqual(len(insert_calls), 0, "Must not insert duplicate rule")
|
||||
|
||||
def test_apply_cell_rules_calls_ensure_forward_stateful(self):
|
||||
"""apply_cell_rules must call ensure_forward_stateful so replies are never dropped."""
|
||||
with patch.object(firewall_manager, '_wg_exec', return_value=MagicMock(returncode=0, stdout='')), \
|
||||
patch.object(firewall_manager, '_get_caddy_container_ip', return_value='172.20.0.2'), \
|
||||
patch.object(firewall_manager, '_get_dns_container_ip', return_value='172.20.0.3'), \
|
||||
patch.object(firewall_manager, '_get_cell_api_ip', return_value='172.20.0.10'), \
|
||||
patch.object(firewall_manager, 'ensure_forward_stateful') as mock_stateful:
|
||||
firewall_manager.apply_cell_rules('testcell', '10.0.0.0/24', [])
|
||||
mock_stateful.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user