From d3294552f0ea0de891b3c62e64a5ee8563fc890e Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Mon, 20 Apr 2026 14:02:36 -0400 Subject: [PATCH] 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 --- api/wireguard_manager.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/api/wireguard_manager.py b/api/wireguard_manager.py index e133c61..0bf164a 100644 --- a/api/wireguard_manager.py +++ b/api/wireguard_manager.py @@ -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: