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:
2026-05-02 04:07:10 -04:00
parent 9a800e3b6b
commit f1666ba19c
5 changed files with 169 additions and 9 deletions
+66
View File
@@ -434,6 +434,8 @@ class TestWireGuardConfigReads(unittest.TestCase):
with patch.object(self.wg, 'get_external_ip', return_value='1.2.3.4'):
cfg = self.wg.get_server_config()
self.assertIn('dns_ip', cfg)
# dns_ip must be the WG server IP, not the container IP
self.assertEqual(cfg['dns_ip'], '10.2.0.1')
self.assertIn('split_tunnel_ips', cfg)
self.assertIn('10.2.0.0/24', cfg['split_tunnel_ips'])
@@ -486,6 +488,70 @@ class TestWireGuardSysctlAndPortCheck(unittest.TestCase):
self.assertIn('FORWARD -i %i -j DROP', cfg)
self.assertNotIn('FORWARD -i %i -j ACCEPT', cfg)
def test_generate_config_includes_dns_dnat_in_postup(self):
cfg = self.wg.generate_config()
self.assertIn('--dport 53 -j DNAT', cfg)
self.assertIn('--dport 80 -j DNAT', cfg)
def test_generate_config_postdown_removes_dnat(self):
cfg = self.wg.generate_config()
postdown_line = [l for l in cfg.splitlines() if l.startswith('PostDown')][0]
self.assertIn('--dport 53 -j DNAT', postdown_line)
self.assertIn('--dport 80 -j DNAT', postdown_line)
# ── ensure_postup_dnat ────────────────────────────────────────────────────
def _write_wg_conf_postup(self, address='10.0.0.1/24', extra_postup=''):
import os
wg_dir = os.path.join(self.test_dir, 'wireguard', 'wg_confs')
os.makedirs(wg_dir, exist_ok=True)
conf_path = os.path.join(wg_dir, 'wg0.conf')
postup = (
'iptables -A FORWARD -i %i -j DROP; '
'iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE'
)
if extra_postup:
postup += f'; {extra_postup}'
postdown = (
'iptables -D FORWARD -i %i -j DROP; '
'iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE'
)
content = (
'[Interface]\n'
f'Address = {address}\n'
f'PostUp = {postup}\n'
f'PostDown = {postdown}\n'
)
with open(conf_path, 'w') as f:
f.write(content)
return conf_path
@patch('subprocess.run')
def test_ensure_postup_dnat_adds_rules_when_missing(self, mock_run):
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = '172.20.0.3'
self._write_wg_conf_postup()
changed = self.wg.ensure_postup_dnat()
self.assertTrue(changed)
with open(self.wg._config_file()) as f:
content = f.read()
self.assertIn('--dport 53 -j DNAT', content)
self.assertIn('--dport 80 -j DNAT', content)
@patch('subprocess.run')
def test_ensure_postup_dnat_idempotent_when_rules_present(self, mock_run):
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = '172.20.0.3'
self._write_wg_conf_postup(
extra_postup='iptables -t nat -A PREROUTING -i %i -p udp --dport 53 -j DNAT --to-destination 172.20.0.3:53'
)
changed = self.wg.ensure_postup_dnat()
self.assertFalse(changed)
def test_ensure_postup_dnat_returns_false_when_no_conf(self):
changed = self.wg.ensure_postup_dnat()
self.assertFalse(changed)
# ── check_port_open ───────────────────────────────────────────────────────
@patch('subprocess.run')