feat: fix cross-cell service access — DNS DNAT, service DNAT, Caddy routing
DNS A records now return the WireGuard server IP (10.0.0.1) instead of Docker bridge VIPs so cross-cell peers resolve service names correctly regardless of their bridge subnet. DNAT rules (wg0:53→cell-dns:53 and wg0:80→cell-caddy:80) are applied at startup. Caddy routes by Host header, eliminating the Docker bridge subnet conflict. Firewall cell rules allow DNS and service (Caddy) traffic from linked cell subnets. Split-tunnel AllowedIPs now dynamically includes connected-cell VPN subnets and drops the 172.20.0.0/16 range. Peers with route_via set now receive full-tunnel config (0.0.0.0/0) so all their traffic exits via the remote cell. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -465,10 +465,15 @@ class TestPeerEndpointAccessControl:
|
||||
class TestDNSZoneRecords:
|
||||
"""
|
||||
Verify that network_manager._build_dns_records() generates the correct IPs.
|
||||
api and webui must point to Caddy (not their container IPs) so Caddy can
|
||||
reverse-proxy them — their containers don't listen on port 80.
|
||||
|
||||
All service names now resolve to the WG server IP (10.0.0.1) rather than
|
||||
Docker VIPs. ensure_service_dnat() routes wg0:80 → Caddy; Caddy routes to
|
||||
the correct backend by Host header. This allows cross-cell peers to reach
|
||||
services without Docker bridge subnet conflicts.
|
||||
"""
|
||||
|
||||
_WG_SERVER_IP = '10.0.0.1'
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@@ -477,59 +482,59 @@ class TestDNSZoneRecords:
|
||||
mgr = nm.NetworkManager.__new__(nm.NetworkManager)
|
||||
return mgr._build_dns_records(cell_name, ip_range)
|
||||
|
||||
def test_api_resolves_to_caddy_not_api_container(self):
|
||||
def test_api_resolves_to_wg_server_ip(self):
|
||||
records = self._records()
|
||||
api_rec = next((r for r in records if r['name'] == 'api'), None)
|
||||
assert api_rec is not None, "No DNS record for 'api'"
|
||||
assert api_rec['value'] == '172.20.0.2', (
|
||||
f"api.dev should resolve to Caddy (172.20.0.2), not the API container "
|
||||
f"(172.20.0.10); got {api_rec['value']}"
|
||||
assert api_rec['value'] == self._WG_SERVER_IP, (
|
||||
f"api.dev should resolve to WG server IP ({self._WG_SERVER_IP}); "
|
||||
f"got {api_rec['value']}"
|
||||
)
|
||||
|
||||
def test_webui_resolves_to_caddy_not_webui_container(self):
|
||||
def test_webui_resolves_to_wg_server_ip(self):
|
||||
records = self._records()
|
||||
rec = next((r for r in records if r['name'] == 'webui'), None)
|
||||
assert rec is not None, "No DNS record for 'webui'"
|
||||
assert rec['value'] == '172.20.0.2', (
|
||||
f"webui.dev should resolve to Caddy (172.20.0.2), not the WebUI container "
|
||||
f"(172.20.0.11); got {rec['value']}"
|
||||
assert rec['value'] == self._WG_SERVER_IP, (
|
||||
f"webui.dev should resolve to WG server IP ({self._WG_SERVER_IP}); "
|
||||
f"got {rec['value']}"
|
||||
)
|
||||
|
||||
def test_calendar_uses_vip(self):
|
||||
def test_calendar_resolves_to_wg_server_ip(self):
|
||||
records = self._records()
|
||||
rec = next((r for r in records if r['name'] == 'calendar'), None)
|
||||
assert rec and rec['value'] == '172.20.0.21', \
|
||||
f"calendar.dev VIP should be 172.20.0.21; got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"calendar.dev should resolve to WG server IP; got {rec}"
|
||||
|
||||
def test_files_uses_vip(self):
|
||||
def test_files_resolves_to_wg_server_ip(self):
|
||||
records = self._records()
|
||||
rec = next((r for r in records if r['name'] == 'files'), None)
|
||||
assert rec and rec['value'] == '172.20.0.22', \
|
||||
f"files.dev VIP should be 172.20.0.22; got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"files.dev should resolve to WG server IP; got {rec}"
|
||||
|
||||
def test_mail_uses_vip(self):
|
||||
def test_mail_resolves_to_wg_server_ip(self):
|
||||
records = self._records()
|
||||
rec = next((r for r in records if r['name'] == 'mail'), None)
|
||||
assert rec and rec['value'] == '172.20.0.23', \
|
||||
f"mail.dev VIP should be 172.20.0.23; got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"mail.dev should resolve to WG server IP; got {rec}"
|
||||
|
||||
def test_webmail_uses_mail_vip(self):
|
||||
def test_webmail_resolves_to_wg_server_ip(self):
|
||||
records = self._records()
|
||||
rec = next((r for r in records if r['name'] == 'webmail'), None)
|
||||
assert rec and rec['value'] == '172.20.0.23', \
|
||||
f"webmail.dev should share the mail VIP 172.20.0.23; got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"webmail.dev should resolve to WG server IP; got {rec}"
|
||||
|
||||
def test_webdav_uses_vip(self):
|
||||
def test_webdav_resolves_to_wg_server_ip(self):
|
||||
records = self._records()
|
||||
rec = next((r for r in records if r['name'] == 'webdav'), None)
|
||||
assert rec and rec['value'] == '172.20.0.24', \
|
||||
f"webdav.dev VIP should be 172.20.0.24; got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"webdav.dev should resolve to WG server IP; got {rec}"
|
||||
|
||||
def test_cell_name_resolves_to_caddy(self):
|
||||
def test_cell_name_resolves_to_wg_server_ip(self):
|
||||
records = self._records(cell_name='mypic')
|
||||
rec = next((r for r in records if r['name'] == 'mypic'), None)
|
||||
assert rec and rec['value'] == '172.20.0.2', \
|
||||
f"mypic.dev should resolve to Caddy (172.20.0.2); got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"mypic.dev should resolve to WG server IP; got {rec}"
|
||||
|
||||
def test_all_records_are_type_a(self):
|
||||
records = self._records()
|
||||
@@ -540,21 +545,23 @@ class TestDNSZoneRecords:
|
||||
class TestDNSZoneRecordsWithPytest:
|
||||
"""Same as above but using pytest-style (no setUp/tearDown)."""
|
||||
|
||||
_WG_SERVER_IP = '10.0.0.1'
|
||||
|
||||
@pytest.fixture
|
||||
def records(self):
|
||||
import network_manager as nm
|
||||
mgr = nm.NetworkManager.__new__(nm.NetworkManager)
|
||||
return mgr._build_dns_records('pic0', '172.20.0.0/16')
|
||||
|
||||
def test_api_resolves_to_caddy(self, records):
|
||||
def test_api_resolves_to_wg_server_ip(self, records):
|
||||
rec = next((r for r in records if r['name'] == 'api'), None)
|
||||
assert rec and rec['value'] == '172.20.0.2', \
|
||||
f"api.dev should point to Caddy (172.20.0.2); got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"api.dev should point to WG server IP ({self._WG_SERVER_IP}); got {rec}"
|
||||
|
||||
def test_webui_resolves_to_caddy(self, records):
|
||||
def test_webui_resolves_to_wg_server_ip(self, records):
|
||||
rec = next((r for r in records if r['name'] == 'webui'), None)
|
||||
assert rec and rec['value'] == '172.20.0.2', \
|
||||
f"webui.dev should point to Caddy (172.20.0.2); got {rec}"
|
||||
assert rec and rec['value'] == self._WG_SERVER_IP, \
|
||||
f"webui.dev should point to WG server IP ({self._WG_SERVER_IP}); got {rec}"
|
||||
|
||||
|
||||
# ─────────────────── Caddyfile generation ─────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user