fix: make cross-cell peer-sync push actually reach the remote cell's API
Unit Tests / test (push) Successful in 9m48s

The offer/permission push between linked cells never worked end-to-end. Two
fixes complete the transport (the push already targets the remote over the WG
tunnel; fix #3 earlier pointed it at HTTPS):

1. The slim WireGuard image (where the push originates — the only namespace with
   routes to remote-cell VPN subnets) had no TLS-capable HTTP client (busybox
   wget lacks TLS, no curl). Add curl + ca-certificates (~5MB).

2. The receiving cell's cell-link firewall allowed the linked subnet to reach
   cell-api:3000 — a dead path (the API binds 127.0.0.1 only; nothing DNATs
   :3000). Move the peer-sync ACCEPT to Caddy:443, which the WG server already
   DNATs (wg0:443 → Caddy → cell-api) and whose replies the existing
   `-o eth0 MASQUERADE` routes back through the tunnel. Source auth (cell VPN
   subnet via X-Forwarded-For) is preserved; the API stays 127.0.0.1-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 10:01:56 -04:00
parent c7e01d4aa7
commit 714fb9b1a9
3 changed files with 61 additions and 38 deletions
+41 -28
View File
@@ -702,32 +702,45 @@ class TestCellRules(unittest.TestCase):
]
self.assertTrue(subnet_drops, "Expected a catch-all DROP rule for the subnet")
def test_apply_cell_rules_sends_accept_for_allowed_service(self):
"""apply_cell_rules inserts Caddy ACCEPT when inbound_services is non-empty."""
calls = self._capture_apply('office', '10.0.1.0/24', ['calendar'])
caddy_targets = self._targets_for_dest(calls, self._FAKE_CADDY_IP)
self.assertIn('ACCEPT', caddy_targets,
"Expected ACCEPT to Caddy when inbound_services is non-empty")
def test_apply_cell_rules_no_caddy_accept_when_no_inbound(self):
"""apply_cell_rules does NOT insert Caddy ACCEPT when inbound_services is empty."""
calls = self._capture_apply('office', '10.0.1.0/24', [])
caddy_targets = self._targets_for_dest(calls, self._FAKE_CADDY_IP)
self.assertNotIn('ACCEPT', caddy_targets,
"No Caddy ACCEPT expected when inbound_services is empty")
def test_apply_cell_rules_accepts_api_sync_traffic(self):
"""apply_cell_rules inserts ACCEPT for cell-api:3000 so permission-sync pushes pass."""
calls = self._capture_apply('office', '10.0.1.0/24', [])
api_ip = self._FAKE_API_IP
api_accepts = [
def _caddy_accepts_on_port(self, calls, port):
"""Caddy-dest ACCEPT calls matching --dport <port>."""
return [
c for c in calls
if '-s' in c and '10.0.1.0/24' in c
and '-d' in c and api_ip in c
and '--dport' in c and '3000' in c
if '-d' in c and self._FAKE_CADDY_IP in c
and '--dport' in c and str(port) in c
and '-j' in c and c[c.index('-j') + 1] == 'ACCEPT'
]
self.assertTrue(api_accepts, 'Expected an ACCEPT rule for cell-api:3000')
def test_apply_cell_rules_sends_accept_for_allowed_service(self):
"""apply_cell_rules inserts a Caddy:80 ACCEPT when inbound_services is non-empty."""
calls = self._capture_apply('office', '10.0.1.0/24', ['calendar'])
self.assertTrue(self._caddy_accepts_on_port(calls, 80),
"Expected ACCEPT to Caddy:80 for an inbound service")
def test_apply_cell_rules_no_service_accept_when_no_inbound(self):
"""No Caddy:80 (service) ACCEPT when inbound_services is empty.
The :443 peer-sync ACCEPT is separate and always present (below).
"""
calls = self._capture_apply('office', '10.0.1.0/24', [])
self.assertFalse(self._caddy_accepts_on_port(calls, 80),
"No Caddy:80 service ACCEPT expected with empty inbound")
def test_apply_cell_rules_accepts_peer_sync_to_caddy_443(self):
"""Cross-cell peer-sync ACCEPT to Caddy:443 is always added (the push reaches
cell-api through Caddy, since the API binds 127.0.0.1 only)."""
calls = self._capture_apply('office', '10.0.1.0/24', [])
peer_sync = [
c for c in self._caddy_accepts_on_port(calls, 443)
if '-s' in c and '10.0.1.0/24' in c
]
self.assertTrue(peer_sync, 'Expected ACCEPT to Caddy:443 for peer-sync')
# And it must NOT target the (127.0.0.1-only) cell-api on :3000 anymore.
api_3000 = [
c for c in calls
if '-d' in c and self._FAKE_API_IP in c and '--dport' in c and '3000' in c
]
self.assertFalse(api_3000, 'Peer-sync must not target cell-api:3000')
def test_apply_cell_rules_api_sync_accept_before_catchall_drop(self):
"""The API-sync ACCEPT must be inserted after service rules so it ends up above DROP."""
@@ -754,12 +767,12 @@ class TestCellRules(unittest.TestCase):
# ── apply_cell_rules — empty inbound (all-deny) ───────────────────────────
def test_apply_cell_rules_empty_inbound_no_service_accept(self):
"""With inbound_services=[], no service ACCEPT is added; catch-all DROP blocks traffic."""
"""With inbound_services=[], no Caddy:80 service ACCEPT is added; the catch-all
DROP blocks service traffic (only the :443 peer-sync ACCEPT is present)."""
calls = self._capture_apply('office', '10.0.1.0/24', [])
# No ACCEPT to Caddy
caddy_targets = self._targets_for_dest(calls, self._FAKE_CADDY_IP)
self.assertNotIn('ACCEPT', caddy_targets,
"No Caddy ACCEPT expected with empty inbound_services")
# No service ACCEPT to Caddy on :80
self.assertFalse(self._caddy_accepts_on_port(calls, 80),
"No Caddy:80 ACCEPT expected with empty inbound_services")
# No per-VIP rules at all
for service, svc_ip in firewall_manager.SERVICE_IPS.items():
svc_targets = self._targets_for_dest(calls, svc_ip)