Illia Nikitchenko

Home / Cases

Works from outside, fails from inside: NAT hairpin

A WireGuard server that answered perfectly over mobile data, but not from the home Wi-Fi when I used its domain name.

Setup

Phone (4G) Laptop (LAN) home Wi-Fi Router public IP + NAT WireGuard server 192.168.x.x via public IP: works via public IP: no reply
Same domain name, same port. Only the network the client sits on differs.

Symptom

Over mobile data, the tunnel came up, and SSH and ping through it worked. From the home Wi-Fi with the same config, the handshake never completed.

Hypotheses I checked

HypothesisHow I checkedResult
ufw blocks 51820/UDPsudo ufw statusRule present, and mobile clients connected, so ruled out
Port forward is wrongTest from mobile dataWorks from outside, so ruled out
Dynamic DNS name is staleCompare dig +short my-domain.example with the router's WAN IPMatched, so ruled out
Key or config mistake on the clientSame config on the phone over 4GWorks, so ruled out
Router does not loop internal traffic back (NAT hairpin)Connect to the server's local IP instead of the domainWorks, which points to this

Diagnosis

A useful check is to watch the WireGuard port on the server while a client connects:

sudo tcpdump -ni any udp port 51820

From mobile data, packets show up. From the LAN via the domain name, nothing arrives. The packets never reach the server, so the problem is not WireGuard or the firewall.

Root cause

The domain resolves to the router's public IP. A LAN client sends its packet to that address, which is the router itself. For this to work, the router must recognise that the destination is its own public IP, apply the port-forward rule, and send the packet back into the LAN to the server. This is called NAT hairpinning (or NAT loopback). Many consumer routers, including my ISP's, do not support it. This is a limitation of the router, not a bug in my configuration.

Fix

Use two endpoints depending on where the client is:

# Inside the home network
Endpoint = 192.168.x.x:51820

# Outside the home network
Endpoint = my-domain.example:51820

Other options I considered:

What I learned

← Back to all cases