fix: embed DNAT rules in wg0.conf PostUp for persistence + fix dns_ip in server config
DNAT rules applied via docker exec are lost whenever wg-easy reloads the WireGuard interface (PostDown flushes the nat table then PostUp only re-adds static rules). Fix: embed DNS (port 53) and service (port 80) DNAT rules directly in wg0.conf PostUp/PostDown so they reapply on every interface restart. ensure_postup_dnat() patches existing configs on startup. get_server_config() now returns the WG server IP (e.g. 10.0.0.1) for dns_ip instead of the cell-dns container IP (172.20.0.3). This makes the value consistent with what get_peer_config() writes into the .conf file, and fixes the stale hint text in Peers.jsx and WireGuard.jsx. UI: fallback dns_ip changed from 172.20.0.3 to 10.0.0.1; split-tunnel fallback drops the 172.20.0.0/16 stale range. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,19 @@ class WireGuardManager(BaseServiceManager):
|
||||
"""Return server config (alias for generate_config, returns dict for API compat)."""
|
||||
return {'config': self.generate_config(interface, port)}
|
||||
|
||||
def _get_dnat_container_ips(self) -> tuple:
|
||||
"""Return (dns_ip, caddy_ip) by inspecting running containers."""
|
||||
def _inspect(name, fallback):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['docker', 'inspect', '--format',
|
||||
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', name],
|
||||
capture_output=True, text=True, check=False)
|
||||
return r.stdout.strip() or fallback
|
||||
except Exception:
|
||||
return fallback
|
||||
return _inspect('cell-dns', '172.20.0.3'), _inspect('cell-caddy', '172.20.0.2')
|
||||
|
||||
def generate_config(self, interface: str = 'wg0', port: int = DEFAULT_PORT) -> str:
|
||||
"""Return a WireGuard [Interface] config string for the server."""
|
||||
import ipaddress
|
||||
@@ -129,6 +142,23 @@ class WireGuardManager(BaseServiceManager):
|
||||
if ext_ip else ''
|
||||
)
|
||||
cfg_port = self._get_configured_port() if os.path.exists(self._config_file()) else port
|
||||
dns_ip, caddy_ip = self._get_dnat_container_ips()
|
||||
dnat_up = (
|
||||
f'iptables -t nat -A PREROUTING -i %i -p udp --dport 53 -j DNAT --to-destination {dns_ip}:53; '
|
||||
f'iptables -t nat -A PREROUTING -i %i -p tcp --dport 53 -j DNAT --to-destination {dns_ip}:53; '
|
||||
f'iptables -t nat -A PREROUTING -i %i -p tcp --dport 80 -j DNAT --to-destination {caddy_ip}:80; '
|
||||
f'iptables -I FORWARD -i %i -o eth0 -p tcp --dport 80 -j ACCEPT; '
|
||||
f'iptables -I FORWARD -i %i -o eth0 -p udp --dport 53 -j ACCEPT; '
|
||||
f'iptables -I FORWARD -i %i -o eth0 -p tcp --dport 53 -j ACCEPT'
|
||||
)
|
||||
dnat_down = (
|
||||
f'iptables -t nat -D PREROUTING -i %i -p udp --dport 53 -j DNAT --to-destination {dns_ip}:53 2>/dev/null || true; '
|
||||
f'iptables -t nat -D PREROUTING -i %i -p tcp --dport 53 -j DNAT --to-destination {dns_ip}:53 2>/dev/null || true; '
|
||||
f'iptables -t nat -D PREROUTING -i %i -p tcp --dport 80 -j DNAT --to-destination {caddy_ip}:80 2>/dev/null || true; '
|
||||
f'iptables -D FORWARD -i %i -o eth0 -p tcp --dport 80 -j ACCEPT 2>/dev/null || true; '
|
||||
f'iptables -D FORWARD -i %i -o eth0 -p udp --dport 53 -j ACCEPT 2>/dev/null || true; '
|
||||
f'iptables -D FORWARD -i %i -o eth0 -p tcp --dport 53 -j ACCEPT 2>/dev/null || true'
|
||||
)
|
||||
return (
|
||||
f'[Interface]\n'
|
||||
f'PrivateKey = {keys["private_key"]}\n'
|
||||
@@ -137,13 +167,69 @@ class WireGuardManager(BaseServiceManager):
|
||||
f'PostUp = iptables -A FORWARD -i %i -j DROP; '
|
||||
f'iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; '
|
||||
f'{hairpin}'
|
||||
f'{dnat_up}; '
|
||||
f'sysctl -q net.ipv4.conf.all.rp_filter=0 || true\n'
|
||||
f'PostDown = iptables -D FORWARD -i %i -j DROP; '
|
||||
f'iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; '
|
||||
f'PostDown = iptables -D FORWARD -i %i -j DROP 2>/dev/null || true; '
|
||||
f'iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null || true; '
|
||||
f'{hairpin_down}'
|
||||
f'{dnat_down}; '
|
||||
f'sysctl -q net.ipv4.conf.all.rp_filter=1 || true\n'
|
||||
)
|
||||
|
||||
def ensure_postup_dnat(self) -> bool:
|
||||
"""Update wg0.conf PostUp/PostDown to include DNS (53) and service (80) DNAT rules.
|
||||
|
||||
Called at startup so rules persist across WireGuard interface restarts.
|
||||
Returns True if the file was changed (caller should reload WG config).
|
||||
"""
|
||||
cf = self._config_file()
|
||||
if not os.path.exists(cf):
|
||||
return False
|
||||
with open(cf) as f:
|
||||
content = f.read()
|
||||
|
||||
dns_ip, caddy_ip = self._get_dnat_container_ips()
|
||||
dnat_marker = f'--dport 53 -j DNAT --to-destination {dns_ip}:53'
|
||||
if dnat_marker in content:
|
||||
return False
|
||||
|
||||
dnat_up = (
|
||||
f'iptables -t nat -A PREROUTING -i %i -p udp --dport 53 -j DNAT --to-destination {dns_ip}:53; '
|
||||
f'iptables -t nat -A PREROUTING -i %i -p tcp --dport 53 -j DNAT --to-destination {dns_ip}:53; '
|
||||
f'iptables -t nat -A PREROUTING -i %i -p tcp --dport 80 -j DNAT --to-destination {caddy_ip}:80; '
|
||||
f'iptables -I FORWARD -i %i -o eth0 -p tcp --dport 80 -j ACCEPT; '
|
||||
f'iptables -I FORWARD -i %i -o eth0 -p udp --dport 53 -j ACCEPT; '
|
||||
f'iptables -I FORWARD -i %i -o eth0 -p tcp --dport 53 -j ACCEPT'
|
||||
)
|
||||
dnat_down = (
|
||||
f'iptables -t nat -D PREROUTING -i %i -p udp --dport 53 -j DNAT --to-destination {dns_ip}:53 2>/dev/null || true; '
|
||||
f'iptables -t nat -D PREROUTING -i %i -p tcp --dport 53 -j DNAT --to-destination {dns_ip}:53 2>/dev/null || true; '
|
||||
f'iptables -t nat -D PREROUTING -i %i -p tcp --dport 80 -j DNAT --to-destination {caddy_ip}:80 2>/dev/null || true; '
|
||||
f'iptables -D FORWARD -i %i -o eth0 -p tcp --dport 80 -j ACCEPT 2>/dev/null || true; '
|
||||
f'iptables -D FORWARD -i %i -o eth0 -p udp --dport 53 -j ACCEPT 2>/dev/null || true; '
|
||||
f'iptables -D FORWARD -i %i -o eth0 -p tcp --dport 53 -j ACCEPT 2>/dev/null || true'
|
||||
)
|
||||
|
||||
lines = content.split('\n')
|
||||
updated = []
|
||||
changed = False
|
||||
for line in lines:
|
||||
if line.startswith('PostUp = ') and dnat_marker not in line:
|
||||
updated.append(line + '; ' + dnat_up)
|
||||
changed = True
|
||||
elif line.startswith('PostDown = ') and '--dport 53 -j DNAT' not in line:
|
||||
updated.append(line + '; ' + dnat_down)
|
||||
changed = True
|
||||
else:
|
||||
updated.append(line)
|
||||
|
||||
if changed:
|
||||
with open(cf, 'w') as f:
|
||||
f.write('\n'.join(updated))
|
||||
logger.info(f'ensure_postup_dnat: updated wg0.conf with DNAT rules '
|
||||
f'(dns={dns_ip}, caddy={caddy_ip})')
|
||||
return changed
|
||||
|
||||
def _config_file(self) -> str:
|
||||
# linuxserver/wireguard stores configs in wg_confs/
|
||||
wg_confs = os.path.join(self.wireguard_dir, 'wg_confs')
|
||||
@@ -841,17 +927,22 @@ class WireGuardManager(BaseServiceManager):
|
||||
|
||||
def get_server_config(self) -> Dict[str, Any]:
|
||||
"""Return server public key, external IP, endpoint, port, and tunnel info."""
|
||||
import ipaddress as _ipaddress
|
||||
keys = self.get_keys()
|
||||
external_ip = self.get_external_ip()
|
||||
port = self._get_configured_port()
|
||||
endpoint = f'{external_ip}:{port}' if external_ip else None
|
||||
try:
|
||||
dns_ip = str(_ipaddress.ip_interface(self._get_configured_address()).ip)
|
||||
except Exception:
|
||||
dns_ip = _resolve_peer_dns()
|
||||
return {
|
||||
'public_key': keys['public_key'],
|
||||
'external_ip': external_ip,
|
||||
'endpoint': endpoint,
|
||||
'port': port,
|
||||
'port_open': None,
|
||||
'dns_ip': _resolve_peer_dns(),
|
||||
'dns_ip': dns_ip,
|
||||
'split_tunnel_ips': self.get_split_tunnel_ips(),
|
||||
'vpn_network': self._get_configured_network(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user