fix: hairpin DNAT rule to eliminate VPN ping jitter to server public IP

When a full-tunnel VPN client pings the server's own public IP, traffic
loops out through Docker's external interface and back, causing 60-120ms
jitter. The DNAT PostUp rule intercepts packets from wg0 destined for the
public IP and redirects them to 10.0.0.1 (the VPN interface), keeping
traffic entirely inside the tunnel.

Also updates SERVER_ADDRESS from 172.20.0.1/16 to 10.0.0.1/24 to avoid
routing conflict with the Docker bridge network on eth0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 14:02:36 -04:00
parent e79ee08c63
commit d3294552f0
+19 -4
View File
@@ -22,8 +22,8 @@ except ImportError:
logger = logging.getLogger(__name__)
SERVER_ADDRESS = '172.20.0.1/16'
SERVER_NETWORK = '172.20.0.0/16'
SERVER_ADDRESS = '10.0.0.1/24'
SERVER_NETWORK = '10.0.0.0/24'
DEFAULT_PORT = 51820
def _resolve_peer_dns() -> str:
@@ -109,15 +109,30 @@ class WireGuardManager(BaseServiceManager):
def generate_config(self, interface: str = 'wg0', port: int = DEFAULT_PORT) -> str:
"""Return a WireGuard [Interface] config string for the server."""
keys = self.get_keys()
ext_ip = self.get_external_ip() or ''
# Hairpin DNAT: redirect VPN clients targeting the server's public IP
# to 10.0.0.1 (the VPN interface), avoiding the Docker network loopback.
hairpin = (
f'iptables -t nat -A PREROUTING -i %i -d {ext_ip} -j DNAT --to-destination 10.0.0.1; '
if ext_ip else ''
)
hairpin_down = (
f'iptables -t nat -D PREROUTING -i %i -d {ext_ip} -j DNAT --to-destination 10.0.0.1; '
if ext_ip else ''
)
return (
f'[Interface]\n'
f'PrivateKey = {keys["private_key"]}\n'
f'Address = {SERVER_ADDRESS}\n'
f'ListenPort = {port}\n'
f'PostUp = iptables -A FORWARD -i %i -j ACCEPT; '
f'iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n'
f'iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; '
f'{hairpin}'
f'sysctl -q net.ipv4.conf.all.rp_filter=0\n'
f'PostDown = iptables -D FORWARD -i %i -j ACCEPT; '
f'iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE\n'
f'iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; '
f'{hairpin_down}'
f'sysctl -q net.ipv4.conf.all.rp_filter=1\n'
)
def _config_file(self) -> str: