Fix Phase 1 permission sync: route push via cell-wireguard + DNAT receive

cell-api has no route to remote WG tunnel IPs — only cell-wireguard does.
Fix _push_permissions_to_remote() to use 'docker exec cell-wireguard curl'
so outbound sync HTTP traverses the WG tunnel from the right namespace.

On the receive side, add ensure_cell_api_dnat() which installs three
iptables rules inside cell-wireguard on startup:
  - PREROUTING DNAT: wg0:3000 → cell-api:3000 (Docker bridge IP)
  - POSTROUTING MASQUERADE: so cell-api's reply routes back via wg0
  - FORWARD ACCEPT: allow the wg0→eth0 forwarded traffic

Called from _apply_startup_enforcement() so rules survive container restarts.
Tests updated to mock subprocess.run instead of urllib.request.urlopen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 13:48:49 -04:00
parent a3d0cd5a48
commit 4ba79fd614
5 changed files with 149 additions and 28 deletions
+15 -11
View File
@@ -522,16 +522,18 @@ class TestPermissionSync(unittest.TestCase):
sent_body = {}
def fake_urlopen(req, timeout=None):
def fake_run(cmd, **kwargs):
import json as _j
sent_body.update(_j.loads(req.data))
resp = MagicMock()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
resp.status = 200
return resp
# Extract the -d payload from curl args
d_idx = cmd.index('-d')
sent_body.update(_j.loads(cmd[d_idx + 1]))
r = MagicMock()
r.returncode = 0
r.stdout = '200'
r.stderr = ''
return r
with patch('urllib.request.urlopen', fake_urlopen):
with patch('subprocess.run', fake_run):
result = self.mgr._push_permissions_to_remote(link, 'home', 'homepubkey=')
self.assertTrue(result['ok'])
@@ -544,9 +546,11 @@ class TestPermissionSync(unittest.TestCase):
def test_push_http_error_returns_not_ok(self):
self._add_office()
link = self.mgr.list_connections()[0]
with patch('urllib.request.urlopen',
side_effect=__import__('urllib.error', fromlist=['HTTPError']).HTTPError(
url='', code=503, msg='Service Unavailable', hdrs=None, fp=None)):
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = '503'
mock_result.stderr = ''
with patch('subprocess.run', return_value=mock_result):
result = self.mgr._push_permissions_to_remote(link, 'home', 'homepubkey=')
self.assertFalse(result['ok'])
self.assertIn('503', result['error'])