fix: add X-Forwarded-For WG IP to peer-sync push curl command

MASQUERADE rewrites the source IP of forwarded packets from
the cell's WG address (10.0.x.1) to cell-wireguard's bridge
IP (172.20.x.9).  The peer-sync endpoint authenticates callers
by checking that the source IP is inside a known cell's vpn_subnet,
so MASQUERADE caused all pushes to fail with 403.

Fix: _push_permissions_to_remote() now calls _local_wg_ip() to
get the local wg0 address and passes it as X-Forwarded-For.
_authenticate_peer_cell() already supports XFF for exactly this
proxying scenario.  Also adds a test verifying the header is present
in the constructed curl command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 15:24:08 -04:00
parent 59927b6ad7
commit 7da0cbb714
2 changed files with 83 additions and 2 deletions
+51 -2
View File
@@ -524,7 +524,12 @@ class TestPermissionSync(unittest.TestCase):
def fake_run(cmd, **kwargs):
import json as _j
# Extract the -d payload from curl args
if '-d' not in cmd:
# ip addr show wg0 — return a fake wg0 address
r = MagicMock()
r.returncode = 0
r.stdout = 'inet 10.0.0.1/24 scope global wg0\n'
return r
d_idx = cmd.index('-d')
sent_body.update(_j.loads(cmd[d_idx + 1]))
r = MagicMock()
@@ -543,6 +548,41 @@ class TestPermissionSync(unittest.TestCase):
# Our outbound=files:True → their inbound=files:True
self.assertTrue(pushed_perms['inbound']['files'])
def test_push_xff_header_carries_local_wg_ip(self):
"""Curl command must include X-Forwarded-For with local WG IP.
MASQUERADE rewrites source to Docker bridge IP. Without XFF the remote
can't match the sender's VPN subnet and returns 403.
"""
self._add_office()
link = self.mgr.list_connections()[0]
captured_cmd = {}
def fake_run(cmd, **kwargs):
if '-d' not in cmd:
r = MagicMock()
r.returncode = 0
r.stdout = ' inet 10.0.0.1/24 scope global wg0\n'
return r
captured_cmd['cmd'] = cmd
r = MagicMock()
r.returncode = 0
r.stdout = '200'
r.stderr = ''
return r
with patch('subprocess.run', fake_run):
self.mgr._push_permissions_to_remote(link, 'home', 'homepubkey=')
cmd = captured_cmd.get('cmd', [])
# X-Forwarded-For header must be in the curl command
self.assertIn('-H', cmd)
xff_idx = [i for i, x in enumerate(cmd) if x == '-H' and i + 1 < len(cmd) and 'X-Forwarded-For' in cmd[i + 1]]
self.assertTrue(xff_idx, 'X-Forwarded-For header missing from curl command')
xff_val = cmd[xff_idx[0] + 1]
self.assertIn('10.0.0.1', xff_val)
def test_push_http_error_returns_not_ok(self):
self._add_office()
link = self.mgr.list_connections()[0]
@@ -550,7 +590,16 @@ class TestPermissionSync(unittest.TestCase):
mock_result.returncode = 0
mock_result.stdout = '503'
mock_result.stderr = ''
with patch('subprocess.run', return_value=mock_result):
def fake_run(cmd, **kwargs):
if '-d' not in cmd:
r = MagicMock()
r.returncode = 0
r.stdout = ' inet 10.0.0.1/24 scope global wg0\n'
return r
return mock_result
with patch('subprocess.run', fake_run):
result = self.mgr._push_permissions_to_remote(link, 'home', 'homepubkey=')
self.assertFalse(result['ok'])
self.assertIn('503', result['error'])