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:
+30
-18
@@ -179,27 +179,39 @@ class NetworkManager(BaseServiceManager):
|
||||
warnings.append(f'apply_ip_range failed: {e}')
|
||||
return {'restarted': restarted, 'warnings': warnings}
|
||||
|
||||
def _build_dns_records(self, cell_name: str, ip_range: str) -> List[Dict]:
|
||||
"""Build the standard set of DNS A records for the given subnet.
|
||||
def _get_wg_server_ip(self) -> str:
|
||||
"""Return the WireGuard server IP by reading wg0.conf. Falls back to 10.0.0.1."""
|
||||
try:
|
||||
import ipaddress
|
||||
conf = os.path.join(self.config_dir, 'wireguard', 'wg_confs', 'wg0.conf')
|
||||
with open(conf) as f:
|
||||
for line in f:
|
||||
stripped = line.strip()
|
||||
if stripped.lower().startswith('address'):
|
||||
addr = stripped.split('=', 1)[1].strip()
|
||||
return str(ipaddress.ip_interface(addr).ip)
|
||||
except Exception:
|
||||
pass
|
||||
return '10.0.0.1'
|
||||
|
||||
All user-facing names resolve to the Caddy reverse proxy (caddy IP) so
|
||||
the Host header is passed through and Caddy routes based on it.
|
||||
Exception: calendar/files/mail/webdav use dedicated virtual IPs so that
|
||||
iptables per-service firewall rules can target them by destination IP.
|
||||
api and webui also go through Caddy — they don't have their own VIPs and
|
||||
their containers don't serve HTTP on port 80.
|
||||
def _build_dns_records(self, cell_name: str, ip_range: str) -> List[Dict]:
|
||||
"""Build the standard set of DNS A records.
|
||||
|
||||
All service names resolve to the WG server IP so they are reachable
|
||||
from both local WG peers and cross-cell peers without Docker bridge
|
||||
subnet conflicts. ensure_service_dnat() routes wg0:80 to Caddy, which
|
||||
routes requests to the correct backend by Host header.
|
||||
"""
|
||||
import ip_utils
|
||||
ips = ip_utils.get_service_ips(ip_range)
|
||||
wg_ip = self._get_wg_server_ip()
|
||||
return [
|
||||
{'name': cell_name, 'type': 'A', 'value': ips['caddy']},
|
||||
{'name': 'api', 'type': 'A', 'value': ips['caddy']},
|
||||
{'name': 'webui', 'type': 'A', 'value': ips['caddy']},
|
||||
{'name': 'calendar', 'type': 'A', 'value': ips['vip_calendar']},
|
||||
{'name': 'files', 'type': 'A', 'value': ips['vip_files']},
|
||||
{'name': 'mail', 'type': 'A', 'value': ips['vip_mail']},
|
||||
{'name': 'webmail', 'type': 'A', 'value': ips['vip_mail']},
|
||||
{'name': 'webdav', 'type': 'A', 'value': ips['vip_webdav']},
|
||||
{'name': cell_name, 'type': 'A', 'value': wg_ip},
|
||||
{'name': 'api', 'type': 'A', 'value': wg_ip},
|
||||
{'name': 'webui', 'type': 'A', 'value': wg_ip},
|
||||
{'name': 'calendar', 'type': 'A', 'value': wg_ip},
|
||||
{'name': 'files', 'type': 'A', 'value': wg_ip},
|
||||
{'name': 'mail', 'type': 'A', 'value': wg_ip},
|
||||
{'name': 'webmail', 'type': 'A', 'value': wg_ip},
|
||||
{'name': 'webdav', 'type': 'A', 'value': wg_ip},
|
||||
]
|
||||
|
||||
def get_dns_records(self, zone: str = 'cell') -> List[Dict]:
|
||||
|
||||
Reference in New Issue
Block a user