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:
@@ -98,9 +98,12 @@ class TestInternetForwardingRules(unittest.TestCase):
|
||||
|
||||
class TestPeerConfigDns(unittest.TestCase):
|
||||
"""
|
||||
Verify that peer client configs include a DNS = <ip> line pointing to the
|
||||
PIC DNS container. Without DNS, the client tunnel has no internet-accessible
|
||||
domain resolution even though packets are forwarded correctly.
|
||||
Verify that peer client configs include a DNS = <wg_server_ip> line.
|
||||
|
||||
DNS is set to the WG server IP (e.g. 10.0.0.1) rather than the Docker
|
||||
cell-dns container IP. ensure_dns_dnat() routes wg0:53 → cell-dns, so
|
||||
peers reach CoreDNS via the WG server IP — works for both split-tunnel
|
||||
(10.0.x.x in AllowedIPs) and cross-cell peers.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
@@ -123,19 +126,20 @@ class TestPeerConfigDns(unittest.TestCase):
|
||||
# Must be a parseable IPv4 address
|
||||
ipaddress.IPv4Address(dns_ip)
|
||||
|
||||
def test_peer_config_dns_defaults_to_cell_dns_ip(self):
|
||||
"""When cell-dns hostname can't be resolved, falls back to 172.20.0.3."""
|
||||
with patch('wireguard_manager.socket.gethostbyname', side_effect=OSError):
|
||||
keys = self.wg.generate_peer_keys('p1')
|
||||
cfg = self.wg.get_peer_config('p1', '10.0.0.5', keys['private_key'])
|
||||
self.assertIn('DNS = 172.20.0.3', cfg)
|
||||
def test_peer_config_dns_uses_wg_server_ip(self):
|
||||
"""DNS in peer config is the WG server IP; ensure_dns_dnat() routes wg0:53 → cell-dns."""
|
||||
keys = self.wg.generate_peer_keys('p1')
|
||||
cfg = self.wg.get_peer_config('p1', '10.0.0.5', keys['private_key'])
|
||||
# Default WG server address is 10.0.0.1/24 when no wg0.conf exists
|
||||
self.assertIn('DNS = 10.0.0.1', cfg)
|
||||
|
||||
def test_peer_config_dns_uses_resolved_hostname(self):
|
||||
"""When cell-dns resolves, its IP is used as the DNS server."""
|
||||
with patch('wireguard_manager.socket.gethostbyname', return_value='172.20.0.3'):
|
||||
def test_peer_config_dns_fallback_to_resolve_on_error(self):
|
||||
"""If WG address parsing fails, _resolve_peer_dns() is used as fallback."""
|
||||
with patch.object(self.wg, '_get_configured_address', return_value='invalid'), \
|
||||
patch('wireguard_manager.socket.gethostbyname', return_value='172.20.0.9'):
|
||||
keys = self.wg.generate_peer_keys('p2')
|
||||
cfg = self.wg.get_peer_config('p2', '10.0.0.6', keys['private_key'])
|
||||
self.assertIn('DNS = 172.20.0.3', cfg)
|
||||
self.assertIn('DNS = 172.20.0.9', cfg)
|
||||
|
||||
def test_resolve_peer_dns_fallback(self):
|
||||
"""_resolve_peer_dns() always returns a string even when DNS lookup fails."""
|
||||
|
||||
Reference in New Issue
Block a user