nftables Hairpin NAT: Fix NAT Loopback When Your Public IP Fails from the LAN
Your port forwarding works perfectly from the Internet.
A remote client can connect to:
https://203.0.113.10
and nftables forwards the connection to:
192.168.20.10
But a workstation inside your own network tries the same hostname and gets:
Connection timed out
or:
Connection refused
This is a classic hairpin NAT problem, also commonly called NAT loopback or NAT reflection.
The confusing part is that nothing may be wrong with your normal Internet-facing DNAT configuration.
The external path:
Internet
↓
Public IP
↓
Linux Router
↓
DNAT
↓
Internal Server
can work perfectly.
The internal path is different:
LAN Client
↓
Public IP
↓
Linux Router
↓
DNAT
↓
Internal Server
↓
???
The return path represented by ??? is where many configurations fail.
This guide explains exactly why, how nftables and connection tracking see the traffic, when SNAT or masquerading is necessary, and when split-horizon DNS is a cleaner solution than hairpin NAT.
If ordinary Internet port forwarding is not working yet, start with CorgiNetwork’s nftables DNAT Port Forwarding Troubleshooting Guide before implementing hairpin NAT.
What Is Hairpin NAT?
Hairpin NAT allows a client located behind a NAT router to reach another internal system using an address that normally represents the router’s external/public endpoint.
Consider:
Public IP:
203.0.113.10
Internal Web Server:
192.168.20.10
LAN Client:
192.168.10.50
External clients use:
203.0.113.10:443
which is translated to:
192.168.20.10:443
Now suppose the LAN client also uses:
https://example.com
and DNS returns:
203.0.113.10
The connection becomes:
192.168.10.50
↓
203.0.113.10:443
↓
Linux Router
↓
192.168.20.10:443
The traffic enters the router and then effectively turns back toward an internal network.
That U-shaped path is the reason for the term hairpin.
Hairpin NAT vs Normal Port Forwarding
Normal Internet DNAT looks like:
REMOTE CLIENT
|
| Internet
↓
203.0.113.10
|
DNAT
↓
192.168.20.10
Hairpin NAT looks like:
INTERNAL CLIENT
192.168.10.50
|
| Public IP
↓
203.0.113.10
|
Linux Router
|
DNAT
↓
192.168.20.10
The destination may ultimately be identical.
The source and return path are not.
That distinction is critical.
Our Example Network
We will use the DMZ architecture developed in Linux DMZ with nftables.
INTERNET
|
|
203.0.113.10
|
Linux Router
+ nftables
/ \
/ \
VLAN 10 VLAN 20
LAN DMZ
| |
192.168.10.50 192.168.20.10
Client Web Server
Router addresses:
WAN:
203.0.113.10
LAN:
192.168.10.1
DMZ:
192.168.20.1
The public web service is:
203.0.113.10:443
Backend:
192.168.20.10:443
First Make Sure External DNAT Works
Do not troubleshoot hairpin NAT until ordinary DNAT has been proven.
From a genuinely external system, test:
curl -vk https://203.0.113.10/
The packet should follow:
Internet
↓
WAN
↓
PREROUTING
↓
DNAT
↓
FORWARD
↓
DMZ
↓
Web Server
If this fails, hairpin NAT is not yet the problem.
Use nftables DNAT Port Forwarding Not Working to verify:
IP forwarding
routing
DNAT
FORWARD rules
conntrack
server firewall
return route
first.
The Existing External DNAT Rule
A normal public HTTPS forward may look like:
nft add rule ip nat prerouting \
iifname "eth0" \
tcp dport 443 \
dnat to 192.168.20.10:443
Notice:
iifname "eth0"
This means the rule applies only when traffic arrives through the WAN interface.
External traffic:
Internet → eth0
matches.
But our LAN client enters through something such as:
lan0
Therefore:
LAN → lan0
does not match:
iifname "eth0"
This is the first common hairpin NAT failure.
DNAT the Internal Request
One solution is to add a carefully scoped rule for internal clients connecting to the public address.
For example:
sudo nft add rule ip nat prerouting \
ip saddr 192.168.10.0/24 \
ip daddr 203.0.113.10 \
tcp dport 443 \
counter \
dnat to 192.168.20.10:443
Now:
192.168.10.50
↓
203.0.113.10:443
can become:
192.168.10.50
↓
192.168.20.10:443
The official nftables NAT documentation explains DNAT through prerouting NAT chains and the stateful NAT model used by nftables.
But this is only half the story.
Why DNAT Alone May Not Be Enough
Consider a topology where the client and server can communicate directly after translation or where the server has a more direct path back to the client.
The client sends:
SOURCE:
192.168.10.50
DESTINATION:
203.0.113.10
After DNAT, the server sees something conceptually like:
SOURCE:
192.168.10.50
DESTINATION:
192.168.20.10
The server replies toward:
192.168.10.50
If that reply bypasses the NAT router in a topology where the client expects the connection to be associated with the public endpoint, the flow can fail.
The intended stateful path is:
Client
↓
NAT Router
↓
Server
Client
↑
NAT Router
↑
Server
not:
Client
↓
NAT Router
↓
Server
|
+----------→ Client
The NAT device needs to see the relevant flow in both directions when the topology requires it.
DNAT + SNAT
A common hairpin-NAT solution is therefore:
DNAT
+
SNAT
The original request:
192.168.10.50
↓
203.0.113.10:443
is transformed so the server sees the connection as arriving from the router.
Conceptually:
Original:
192.168.10.50
↓
203.0.113.10
After NAT:
192.168.20.1
↓
192.168.20.10
The server naturally replies to:
192.168.20.1
so the response returns through the nftables router.
Conntrack can then apply the corresponding reverse translations.
Configure Hairpin Masquerading
Depending on the topology, an example postrouting rule may look like:
sudo nft add rule ip nat postrouting \
ip saddr 192.168.10.0/24 \
ip daddr 192.168.20.10 \
tcp dport 443 \
counter \
masquerade
Now:
LAN Client
|
| Public IP
↓
Router
|
| DNAT
|
| SNAT/Masquerade
↓
DMZ Server
and the reply returns through the router.
The official nftables NAT documentation documents both snat and masquerade, with masquerading deriving the translated source from the outgoing interface.
Do Not Masquerade More Traffic Than Necessary
Avoid an overly broad rule such as:
Everything internal
↓
masquerade
merely to fix one hairpin connection.
A better rule describes the intended relationship:
LAN source
+
DMZ destination
+
published service
↓
hairpin SNAT
Why?
Because unnecessary SNAT removes useful source-address information from the destination server.
Without SNAT, the server may see:
192.168.10.50
With masquerading, it may instead see:
192.168.20.1
That affects:
application logs
security monitoring
rate limiting
auditing
access control
Use source translation because the topology requires it—not because NAT seems like a universal troubleshooting tool.
Connection Tracking Makes This Stateful
nftables NAT works with Netfilter connection tracking.
The official nftables connection-tracking documentation documents connection states and NAT-related conntrack status information.
For our connection:
Client
192.168.10.50
Public Endpoint
203.0.113.10:443
Backend
192.168.20.10:443
conntrack maintains the state needed to associate the packets belonging to the translated connection.
If the conntrack utility is installed:
sudo conntrack -L -p tcp
or:
sudo conntrack -L -p tcp | grep 443
can help inspect the flow.
This is far more informative than assuming that seeing a DNAT rule means the translation is working end to end.
Remember the FORWARD Chain
Hairpin NAT traffic is still routed traffic.
Your filter policy therefore matters.
If you use:
policy drop
you need an appropriate forwarding rule.
For example:
sudo nft add rule inet firewall forward \
ip saddr 192.168.10.0/24 \
ip daddr 192.168.20.10 \
tcp dport 443 \
ct state new \
counter \
accept
And the stateful foundation should include:
ct state established,related accept
This follows the architecture developed in Linux nftables Inter-VLAN Firewall.
Remember:
NAT says:
How should addresses be translated?
Firewall says:
Is this communication permitted?
They are separate decisions.
Complete Packet Walk
Let’s follow a successful connection.
The client sends:
SRC = 192.168.10.50
DST = 203.0.113.10
DPORT = 443
The packet reaches PREROUTING.
DNAT changes:
DST = 203.0.113.10
to:
DST = 192.168.20.10
Linux performs its routing decision.
The packet enters the forwarding path.
Our firewall permits:
192.168.10.0/24
↓
192.168.20.10:443
Then postrouting may apply the required source translation.
The server receives:
SRC = router-side translated address
DST = 192.168.20.10
The reply returns to the router.
Conntrack recognizes the connection and reverses the NAT operations appropriately.
Finally, the client receives a response consistent with the connection it initiated toward:
203.0.113.10:443
That is the core hairpin-NAT process.
Use Counters Before Changing Rules
Add counters to both relevant NAT rules.
DNAT:
sudo nft add rule ip nat prerouting \
ip saddr 192.168.10.0/24 \
ip daddr 203.0.113.10 \
tcp dport 443 \
counter \
dnat to 192.168.20.10:443
Postrouting:
sudo nft add rule ip nat postrouting \
ip saddr 192.168.10.0/24 \
ip daddr 192.168.20.10 \
tcp dport 443 \
counter \
masquerade
Then inspect:
sudo nft list table ip nat
Generate traffic.
Now you can distinguish:
DNAT counter = 0
from:
DNAT counter increases
SNAT counter = 0
and:
both counters increase
Each tells you something different about the packet path.
Troubleshooting: DNAT Counter Is Zero
If the internal DNAT counter stays at zero, verify:
source address
destination public IP
destination port
protocol
input path
DNS result
Check what the client is actually connecting to:
getent ahosts example.com
or:
dig example.com
Perhaps DNS returns:
IPv6
while you are troubleshooting an IPv4 NAT rule.
Or perhaps the hostname resolves internally to:
192.168.20.10
meaning the client is not using the public address at all.
Troubleshooting: DNAT Counter Increases but FORWARD Does Not
Inspect:
sudo nft -a list chain inet firewall forward
Possible causes include:
wrong destination match
wrong port
wrong protocol
earlier drop rule
wrong chain
routing problem
Remember that after DNAT, the packet’s destination is the internal server.
Your forwarding rule needs to reflect the packet state at that stage of processing.
Troubleshooting: FORWARD Counter Increases but Server Sees Nothing
Check the router’s route:
ip route get 192.168.20.10
Then capture on the DMZ interface:
sudo tcpdump -ni dmz0 \
host 192.168.20.10 and tcp port 443
If the packet never leaves the expected interface, investigate:
routing
VLAN configuration
bridge configuration
neighbor resolution
For VLAN problems, use Linux Bridge VLAN Filtering Explained.
Troubleshooting: Server Receives SYN but Client Times Out
Capture:
sudo tcpdump -ni dmz0 \
host 192.168.20.10 and tcp port 443
If you see:
Client/Router → Server : SYN
but no:
Server → Client/Router : SYN-ACK
the problem is probably on the server.
Check:
ss -lntp
and the server’s local firewall:
sudo nft list ruleset
If the server does send SYN-ACK, inspect where it goes.
Troubleshooting the Return Path
On the server:
ip route
Then:
ip route get RETURN_DESTINATION
The key question is:
Does the response return through the nftables router that created the NAT state?
If not, you may have:
asymmetric routing
or a topology requiring source translation.
For asymmetric-routing behavior on Linux, see Linux rp_filter and Asymmetric Routing.
rp_filter Can Complicate Multi-Homed Designs
Linux reverse-path filtering can matter in networks where traffic legitimately arrives through a path different from the kernel’s preferred return route.
Before disabling anything, understand the topology.
Check:
sysctl net.ipv4.conf.all.rp_filter
and interface-specific values.
Do not automatically set:
rp_filter = 0
just because a NAT connection fails.
First prove that reverse-path validation is actually involved.
The Linux rp_filter and Asymmetric Routing article covers this problem separately so that NAT troubleshooting does not turn into indiscriminate sysctl modification.
Hairpin NAT Across VLANs
Hairpin NAT is especially interesting when the client and server belong to separate VLANs.
Example:
VLAN 10
Users
192.168.10.0/24
VLAN 20
DMZ
192.168.20.0/24
Client:
192.168.10.50
Server:
192.168.20.10
The Linux router is already the inter-VLAN gateway.
In this design, the return path may naturally traverse the router because the networks are different.
That means the exact need for SNAT depends on the topology.
Do not apply a same-subnet hairpin recipe blindly to an inter-VLAN environment.
This is why understanding Linux Inter-VLAN Routing with Network Namespaces is valuable before designing the NAT layer.
Same-Subnet Hairpin NAT Can Be More Difficult
Now imagine:
Client:
192.168.20.50
Server:
192.168.20.10
Both are on:
192.168.20.0/24
The client sends the public-address connection through the router.
The router performs DNAT.
But after translation, the server sees that the original client:
192.168.20.50
is on its own local subnet.
The server may therefore reply directly:
Server
|
+----------> Client
instead of:
Server
↓
NAT Router
↓
Client
This is a classic case where hairpin SNAT/masquerading may be required to force the response through the NAT device.
Same-Subnet Example
Original packet:
SRC:
192.168.20.50
DST:
203.0.113.10:443
DNAT only:
SRC:
192.168.20.50
DST:
192.168.20.10:443
Server sees:
192.168.20.50
and knows:
192.168.20.50 is local
so it can reply directly.
With SNAT:
SRC:
192.168.20.1
DST:
192.168.20.10
the server replies to the router:
192.168.20.1
allowing the NAT device to remain in the stateful path.
The Major Trade-Off: Losing the Original Client IP
Hairpin SNAT solves the routing problem, but the backend may now see:
192.168.20.1
instead of:
192.168.20.50
This matters for:
web logs
SIEM data
IDS/IPS
rate limits
application ACLs
audit trails
This trade-off is one reason split DNS is often worth considering.
Split-Horizon DNS: A Cleaner Alternative?
Instead of making internal clients connect to:
203.0.113.10
you can configure internal DNS so:
www.example.com
returns:
192.168.20.10
for internal clients.
External DNS still returns:
203.0.113.10
The result:
External Client
|
www.example.com
|
203.0.113.10
|
DNAT
|
Server
while:
Internal Client
|
www.example.com
|
192.168.20.10
|
Direct/Internal Routing
|
Server
No hairpin NAT is required for that internal connection.
Hairpin NAT vs Split DNS
| Design | Hairpin NAT | Split DNS |
|---|---|---|
| Internal client uses public IP | Yes | No |
| NAT processing internally | Yes | Usually avoided |
| Same hostname internally/externally | Yes | Yes |
| Backend may lose client IP with SNAT | Possible | Often avoidable |
| Requires internal DNS control | No | Yes |
| Useful for literal public-IP access | Yes | No |
Neither architecture is universally correct.
The correct design depends on:
DNS control
network topology
application requirements
logging requirements
certificate design
operational simplicity
HTTPS and Split DNS
A common concern is:
If internal DNS returns a private address, will HTTPS break?
Not inherently.
TLS certificates validate the hostname presented by the client, not whether DNS returned a public or private address.
For example:
https://www.example.com
can resolve externally to:
203.0.113.10
and internally to:
192.168.20.10
while using a certificate valid for:
www.example.com
The application architecture still needs to support the connection correctly.
Use nft monitor trace for Difficult Cases
When counters are insufficient, nftables tracing can reveal the packet’s path through the ruleset.
After configuring tracing for narrowly selected traffic:
sudo nft monitor trace
can help reveal:
PREROUTING
↓
DNAT
↓
FORWARD
↓
POSTROUTING
↓
SNAT
The official nftables tracing documentation explains the nftrace mechanism and nft monitor trace.
This is particularly useful when:
several NAT rules exist
sets/maps are used
multiple VLANs exist
several firewall chains exist
Use nftables Maps for Multiple Services
Suppose you publish:
443 → 192.168.20.10
8443 → 192.168.20.20
9443 → 192.168.20.30
Writing separate rules is reasonable for a small environment.
As the number grows, nftables maps can make the NAT configuration more scalable.
The official nftables Maps documentation explains key/value mappings, while Multiple NATs using nftables maps demonstrates their use for NAT.
This is the same design principle introduced in our CorgiNetwork article on nftables sets and maps:
DATA
↓
MAP
↓
POLICY
rather than hundreds of repeated rules.
Hairpin NAT and Policy Routing
Now consider a firewall with:
WAN1
WAN2
LAN
DMZ
VPN
A hairpin connection may be translated correctly but still fail because a reply is sent according to an unexpected routing table.
Check:
ip rule
Then:
ip route show table all
And test a lookup:
ip route get DESTINATION from SOURCE
For advanced source-dependent routing, use Linux Policy Routing with ip rule and Multiple Routing Tables.
For persistent Ubuntu configurations, Netplan Multiple Default Routes and Policy Routing extends the same architecture.
Hairpin NAT and the Wrong Interface
If Linux unexpectedly chooses the wrong NIC, adding another NAT rule may hide rather than solve the real issue.
Inspect:
ip route get DESTINATION
and:
ip route get DESTINATION from SOURCE
CorgiNetwork’s Linux Traffic Uses the Wrong Interface explains how to troubleshoot route selection systematically.
A Professional Hairpin NAT Troubleshooting Workflow
Follow this order:
1. Prove external DNAT works
↓
2. Check internal DNS result
↓
3. Confirm client uses public IP
↓
4. Capture request on router
↓
5. Check internal DNAT counter
↓
6. Verify route after DNAT
↓
7. Check FORWARD counter
↓
8. Capture packet at server
↓
9. Verify server response
↓
10. Determine return path
↓
11. Check conntrack
↓
12. Decide whether SNAT is required
↓
13. Test again
Do not start by adding masquerade.
First prove why the return path requires it.
Diagnostic Commands
The most useful commands are:
ip -br addr
ip route
ip rule
ip route get DESTINATION
ip neigh
sysctl net.ipv4.ip_forward
sudo nft list ruleset
sudo nft -a list ruleset
sudo nft list table ip nat
sudo conntrack -L
sudo ss -lntp
sudo tcpdump -ni any host SERVER_IP
sudo nft monitor trace
dig example.com
getent ahosts example.com
Each command answers a specific question about the packet path.
Common Mistake: Testing Hairpin NAT Before External DNAT
Wrong troubleshooting order:
External DNAT broken
+
Internal access broken
↓
Configure hairpin NAT
Now you have two problems.
First establish:
External Client
↓
Public IP
↓
Server
✓
Then troubleshoot:
Internal Client
↓
Public IP
↓
Server
separately.
Common Mistake: Broad Masquerading
Avoid turning:
specific hairpin requirement
into:
masquerade all internal traffic
without understanding the consequences.
The rules may start working while:
source visibility
logging accuracy
network design clarity
become worse.
Common Mistake: Ignoring DNS
Sometimes NAT is working perfectly.
The internal client simply resolves:
example.com
to a different address.
Always check:
dig example.com
before changing firewall rules.
Common Mistake: Forgetting IPv6
Suppose:
A record
→ 203.0.113.10
but:
AAAA record
→ IPv6 address
The browser may attempt IPv6.
Your IPv4 hairpin NAT configuration does not automatically govern that connection.
Check both address families and design an explicit IPv6 security policy.
Common Mistake: Assuming NAT Replaces Firewall Policy
A packet can be translated and then dropped.
Therefore:
DNAT success
does not imply:
FORWARD success
This separation is fundamental to understanding nftables.
Production Design Recommendations
For production systems:
✓ Keep external and internal NAT requirements separate
✓ Use narrowly scoped DNAT rules
✓ Use narrowly scoped SNAT when required
✓ Preserve original source addresses when practical
✓ Prefer clear routing over unnecessary NAT
✓ Consider split DNS
✓ Maintain a default-deny firewall policy
✓ Use counters
✓ Use rate-limited logging
✓ Document NAT relationships
✓ Test return paths
✓ Monitor conntrack
✓ Consider IPv6 separately
✓ Back up the nftables ruleset
Frequently Asked Questions
What is nftables hairpin NAT?
Hairpin NAT allows an internal client to access an internal service through an address exposed by the same NAT router, commonly the service’s public IP.
Why does port forwarding work from the Internet but not from my LAN?
The internal connection follows a different packet path.
Common causes include:
WAN-only DNAT rule
missing internal DNAT
forward-chain policy
return-path bypass
missing hairpin SNAT
DNS differences
Do I always need masquerade for hairpin NAT?
No.
It depends on the network topology and return path.
In particular, same-subnet client/server designs often need source translation because the server can otherwise reply directly to the client.
In routed inter-VLAN designs, the router may already be naturally present in the return path.
What is NAT reflection?
NAT reflection and NAT loopback are common names for the same general behavior usually described as hairpin NAT: an internal client reaches an internal service through an externally represented address.
Can split DNS replace hairpin NAT?
For hostname-based access, often yes.
Internal DNS can return the internal server address while public DNS returns the public endpoint.
That can eliminate unnecessary internal NAT.
Is split DNS always better?
No.
Some applications, tests, or architectures specifically require access through the public address.
The choice depends on operational requirements.
How do I see whether my hairpin DNAT rule matches?
Add:
counter
to the rule and inspect:
sudo nft list table ip nat
You can then combine counters with tcpdump and conntrack to determine where the connection stops.
Why does SNAT hide the client’s IP?
SNAT deliberately changes the packet’s source address.
If the firewall translates:
192.168.10.50
to its own address, the backend sees the translated source.
That is an architectural trade-off rather than a bug.
Conclusion
Hairpin NAT becomes much easier to understand when you stop treating it as a special magic firewall feature and instead follow the packet.
The client starts with:
Internal Client
↓
Public Address
The router performs:
DNAT
to reach:
Internal Server
and, when the topology requires it:
SNAT / Masquerade
keeps the response flowing back through the NAT router.
The complete path becomes:
Internal Client
↓
PREROUTING
↓
DNAT
↓
Routing
↓
FORWARD
↓
POSTROUTING
↓
SNAT
↓
Internal Server
↓
Return Traffic
↓
Conntrack
↓
Reverse Translation
↓
Internal Client
But NAT should not be the automatic solution to every internal-access problem.
If internal clients use a hostname, split-horizon DNS may provide a simpler architecture by resolving the same hostname to an internal address inside the network and a public address externally.
The official nftables NAT documentation provides the underlying DNAT, SNAT, and masquerading behavior, while the official nftables connection-tracking documentation explains the state information used by translated connections. When packet flow becomes difficult to understand, the official nftables tracing guide provides the nftrace workflow used with nft monitor trace.
Within CorgiNetwork, start with nftables DNAT Port Forwarding Not Working if normal port forwarding itself is failing.
The broader perimeter architecture is covered in Linux DMZ with nftables, while Linux nftables Inter-VLAN Firewall explains the stateful filtering foundation underneath both designs.
For routing problems, Linux Traffic Uses the Wrong Interface and Linux Policy Routing with ip rule and Multiple Routing Tables provide the next diagnostic layer.
For asymmetric return paths, use Linux rp_filter and Asymmetric Routing.
And when the topology depends on VLAN isolation, Linux Bridge VLAN Filtering Explained provides the Layer 2 foundation.
The CorgiNetwork cluster now forms a progressively deeper troubleshooting path:
VLANs
↓
Inter-VLAN Routing
↓
Stateful nftables
↓
Sets & Maps
↓
DMZ
↓
DNAT
↓
Port-Forwarding Troubleshooting
↓
Hairpin NAT
↓
Advanced Linux Firewall Engineering
The key principle is simple:
If external DNAT works but internal access through the public address fails, troubleshoot the internal packet path and return path before adding more NAT.