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
- Debian 13 server on the home LAN with a static address (
192.168.x.x). - WireGuard listening on
51820/UDP, allowed in ufw. - Home router (ISP-provided) forwards
51820/UDPto the server. - Dynamic DNS name pointing at the home's changing public IP.
- Clients (laptop, phone) configured with
Endpoint = my-domain.example:51820.
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
| Hypothesis | How I checked | Result |
|---|---|---|
| ufw blocks 51820/UDP | sudo ufw status | Rule present, and mobile clients connected, so ruled out |
| Port forward is wrong | Test from mobile data | Works from outside, so ruled out |
| Dynamic DNS name is stale | Compare dig +short my-domain.example with the router's WAN IP | Matched, so ruled out |
| Key or config mistake on the client | Same config on the phone over 4G | Works, so ruled out |
| Router does not loop internal traffic back (NAT hairpin) | Connect to the server's local IP instead of the domain | Works, 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:
- Split DNS: a local DNS record so the domain resolves to the LAN IP inside the network.
- Replace or reconfigure the router with one that supports NAT loopback.
- Reverse the direction: the lab connects out to a WireGuard hub on a VPS, so no port forward on the home router is needed at all. This is the design I chose later.
What I learned
- Before touching configs, find out what differs between the working and the failing case. Here it was only the client's network.
- "Works from outside" is a strong clue: it rules out the server, the firewall and the port forward in one test.
- A DNS name that points to a public IP behaves differently inside and outside the LAN.
- Some problems are limits of the equipment. Knowing when to stop debugging and design around it is part of the job.