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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 DesignsLinux 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 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.
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.
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.
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.
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.
| 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 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.
nft monitor trace for Difficult CasesWhen 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 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.
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.
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.
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.
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.
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.
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.
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.
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.
A packet can be translated and then dropped.
Therefore:
DNAT success does not imply:
FORWARD success This separation is fundamental to understanding nftables.
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 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.
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 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.
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.
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.
No.
Some applications, tests, or architectures specifically require access through the public address.
The choice depends on operational requirements.
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.
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.
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.
AI Agents as Code: Managing AI Agent Configurations, Prompts and Tools with Git AI agents…
Data Center Power Distribution Architecture: UPS, PDU, Redundancy and Rack Power Explained Data center power…
DNSSEC Troubleshooting: Fix SERVFAIL & DNSKEY Errors DNSSEC Troubleshooting: How to Fix SERVFAIL, DS, DNSKEY…
Terraform State Management Explained: Remote State, Locking, Backends and Best Practices Terraform makes infrastructure reproducible…
RTO vs RPO Explained: Disaster Recovery Objectives, Examples and Planning A disaster recovery plan is…
Network Intrusion Detection Explained: NIDS, Sensors, Signatures and Traffic Analysis A firewall can decide whether…