Implementing Hairpin NAT (NAT Loopback) for Local Access to Port-Forwarded Services


3 views

Consider this common network configuration:


[Internet]
   |
[Router/NAT] (Public IP: 203.0.113.42)
   |
[Local Network]
   ├── [Web Server] (192.168.1.100:80)
   └── [Client PC] (192.168.1.50)

When your client (192.168.1.50) tries to access your public IP (203.0.113.42), the router sees this as:

  1. Outbound packet: SRC=192.168.1.50, DST=203.0.113.42
  2. Inbound packet: SRC=203.0.113.42, DST=192.168.1.50

Most consumer routers won't recognize this as loopback traffic and either drop it or fail to route it properly.

pfSense Configuration

Add these NAT rules:


# Enable NAT reflection
nat on em0 inet from (em1:network) to em0 port 80 -> em0 port 80

# Alternative using pfSense GUI:
1. Firewall > NAT > Port Forward
2. Edit your existing rule
3. Enable "NAT Reflection" option

MikroTik RouterOS


/ip firewall nat
add chain=dstnat action=dst-nat to-addresses=192.168.1.100 \
    to-ports=80 protocol=tcp dst-address=203.0.113.42 dst-port=80
add chain=srcnat action=masquerade src-address=192.168.1.0/24 \
    dst-address=192.168.1.100 protocol=tcp dst-port=80

For environments where NAT loopback isn't supported:


# Internal DNS records:
webserver.local. IN A 192.168.1.100

# External DNS records:
webserver.example.com. IN A 203.0.113.42

Use these troubleshooting commands:


# Check port forwarding
nc -zv 203.0.113.42 80

# Trace NAT path
tcpdump -i eth0 port 80

# Verify DNS resolution
dig +short webserver.example.com

When implementing hairpin NAT:

  • Ensure proper firewall rules are in place
  • Monitor for unusual traffic patterns
  • Consider rate limiting internal->external->internal traffic

Imagine you've set up port forwarding on your router to expose internal services (like a web server on 192.168.1.100:80) to the public internet via your public IP 203.0.113.5. Your DNS records point to this public IP. Here's what happens:

External Client:
1. DNS lookup → 203.0.113.5
2. Connects to 203.0.113.5:80 → Router forwards to 192.168.1.100:80 (works)

Internal Client:
1. DNS lookup → 203.0.113.5 (same public IP)
2. Attempts to connect to 203.0.113.5:80 
   → Router sees source/dest on same interface
   → Typically drops the packet (fails)

Most consumer-grade routers don't implement Hairpin NAT (also called NAT loopback) by default due to:

  • Security concerns about internal traffic appearing to come from WAN
  • Additional NAT processing overhead
  • Historical implementations treating LAN→WAN→LAN as an error state

For professional setups, here are configuration examples:

1. pfSense Configuration

# Enable NAT reflection in pfSense:
# System → Advanced → Firewall & NAT → 
#   ✔ Enable NAT Reflection for 1:1 NAT
#   ✔ Enable Automatic outbound NAT for Reflection

2. Linux iptables Solution

# Enable NAT loopback for HTTP (adjust interfaces as needed):
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to 192.168.1.100
iptables -t nat -A POSTROUTING -o eth1 -p tcp --dport 80 -d 192.168.1.100 -j SNAT --to 192.168.1.1

Split Horizon DNS

Configure internal DNS to return private IPs:

# Example BIND zone configuration:
@ IN A 192.168.1.100    ; Internal clients
@ IN A 203.0.113.5      ; External clients

Proxy Server Solution

For complex setups, consider a reverse proxy:

# Nginx configuration example:
server {
    listen 80;
    server_name example.com;
    
    location / {
        proxy_pass http://192.168.1.100;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Ubiquiti EdgeRouter

configure
set service nat rule 5010 description "Hairpin NAT for Web Server"
set service nat rule 5010 type destination
set service nat rule 5010 inbound-interface eth0
set service nat rule 5010 protocol tcp
set service nat rule 5010 destination port 80
set service nat rule 5010 inside-address address 192.168.1.100
set service nat rule 5010 inside-address port 80
commit
save