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:
@@ -22,8 +22,8 @@ except ImportError:
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SERVER_ADDRESS = '172.20.0.1/16'
|
SERVER_ADDRESS = '10.0.0.1/24'
|
||||||
SERVER_NETWORK = '172.20.0.0/16'
|
SERVER_NETWORK = '10.0.0.0/24'
|
||||||
DEFAULT_PORT = 51820
|
DEFAULT_PORT = 51820
|
||||||
|
|
||||||
def _resolve_peer_dns() -> str:
|
def _resolve_peer_dns() -> str:
|
||||||
@@ -109,15 +109,30 @@ class WireGuardManager(BaseServiceManager):
|
|||||||
def generate_config(self, interface: str = 'wg0', port: int = DEFAULT_PORT) -> str:
|
def generate_config(self, interface: str = 'wg0', port: int = DEFAULT_PORT) -> str:
|
||||||
"""Return a WireGuard [Interface] config string for the server."""
|
"""Return a WireGuard [Interface] config string for the server."""
|
||||||
keys = self.get_keys()
|
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 (
|
return (
|
||||||
f'[Interface]\n'
|
f'[Interface]\n'
|
||||||
f'PrivateKey = {keys["private_key"]}\n'
|
f'PrivateKey = {keys["private_key"]}\n'
|
||||||
f'Address = {SERVER_ADDRESS}\n'
|
f'Address = {SERVER_ADDRESS}\n'
|
||||||
f'ListenPort = {port}\n'
|
f'ListenPort = {port}\n'
|
||||||
f'PostUp = iptables -A FORWARD -i %i -j ACCEPT; '
|
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'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:
|
def _config_file(self) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user