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:
@@ -284,20 +284,24 @@ class TestBootstrapDnsRecords(unittest.TestCase):
|
||||
self.assertTrue(os.path.exists(zone_file))
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_contains_default_caddy_ip(self, _mock):
|
||||
def test_contains_wg_server_ip(self, _mock):
|
||||
"""Zone file records now use WG server IP (10.0.0.1) not Docker VIPs."""
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
content = open(zone_file).read()
|
||||
self.assertIn('172.20.0.2', content) # caddy
|
||||
self.assertIn('10.0.0.1', content) # WG server IP for all services
|
||||
self.assertNotIn('172.20.0.2', content) # Caddy VIP no longer in zone
|
||||
self.assertNotIn('172.20.0.21', content) # Service VIPs no longer in zone
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_custom_ip_range_used(self, _mock):
|
||||
def test_custom_ip_range_does_not_affect_service_ips(self, _mock):
|
||||
"""ip_range is no longer used for service record IPs; WG server IP is used."""
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell', ip_range='10.5.0.0/24')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
content = open(zone_file).read()
|
||||
self.assertIn('10.5.0.2', content) # caddy
|
||||
self.assertIn('10.5.0.21', content) # vip_calendar
|
||||
self.assertNotIn('172.20', content)
|
||||
self.assertIn('10.0.0.1', content) # WG server IP
|
||||
self.assertNotIn('10.5.0.2', content) # old caddy pattern gone
|
||||
self.assertNotIn('10.5.0.21', content) # old VIP pattern gone
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_idempotent_skips_existing_zone(self, _mock):
|
||||
@@ -324,15 +328,15 @@ class TestApplyIpRange(unittest.TestCase):
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_zone_file_updated_with_new_ips(self, _mock):
|
||||
# Bootstrap with default range, then change to 10.0.0.0/24
|
||||
def test_zone_file_updated_with_wg_server_ip(self, _mock):
|
||||
"""apply_ip_range regenerates zone with WG server IP for all service records."""
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell', '172.20.0.0/16')
|
||||
result = self.nm.apply_ip_range('10.0.0.0/24', 'mycell', 'cell')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
content = open(zone_file).read()
|
||||
self.assertIn('10.0.0.2', content) # caddy
|
||||
self.assertIn('10.0.0.21', content) # vip_calendar
|
||||
self.assertNotIn('172.20', content)
|
||||
self.assertIn('10.0.0.1', content) # WG server IP for all services
|
||||
self.assertNotIn('172.20.0.2', content) # old Caddy pattern gone
|
||||
self.assertNotIn('172.20.0.21', content) # old VIP pattern gone
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_returns_restarted_on_success(self, _mock):
|
||||
|
||||
Reference in New Issue
Block a user