Server--:--:--You--:--:--

How NAT Actually Works

By Prabath Thalangama· September 7, 2026· 3 min read
#networking#nat#iptables#troubleshooting

Introduction

NAT lets many private hosts share one public IP. It's so ubiquitous it feels like part of IP itself, but it's a rewrite layer with real limits and quirks — port exhaustion, hairpinning, protocol breakage. Understanding the connection- tracking table behind it explains all of them.

Concepts

Three operations:

  • SNAT (Source NAT) — rewrite the source IP:port of outbound packets. Masquerade is SNAT to "whatever the outgoing interface's IP is" (for dynamic addresses). This is what your home router does.
  • DNAT (Destination NAT) — rewrite the destination of inbound packets. Port forwarding: :80 on the public IP → 10.0.0.5:8080.
  • PAT (Port Address Translation) — many-to-one. Because multiple internal hosts share one external IP, the router also rewrites the port and keeps a mapping so replies can be un-NATed back to the right host.

It's stateful

The mapping lives in the connection tracking table (conntrack on Linux). Each entry: original tuple (src/dst IP:port), reply tuple, protocol, state, timeout. When a reply comes back, conntrack looks up the entry and reverses the translation.

sudo conntrack -L                        # every tracked flow
sudo conntrack -L -p tcp --state ESTABLISHED | wc -l
cat /proc/sys/net/netfilter/nf_conntrack_count   # current
cat /proc/sys/net/netfilter/nf_conntrack_max     # limit

Hands-on

# Masquerade a private LAN out eth0 (nftables shown; iptables in comments)
nft add rule ip nat postrouting oif "eth0" ip saddr 10.0.0.0/24 masquerade
# iptables: -t nat -A POSTROUTING -o eth0 -s 10.0.0.0/24 -j MASQUERADE

# Port-forward public :443 to an internal host
nft add rule ip nat prerouting iif "eth0" tcp dport 443 dnat to 10.0.0.5:8443
# iptables: -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j DNAT --to 10.0.0.5:8443

# Don't forget the forward filter rule
nft add rule ip filter forward ip daddr 10.0.0.5 tcp dport 8443 ct state new,established accept

# Enable forwarding
sysctl -w net.ipv4.ip_forward=1

Hairpin / NAT loopback

An internal host tries to reach the public IP of a service that's also internal. The packet goes to the router, gets DNAT'd back inside — but the reply goes straight from the server to the client (same subnet), bypassing the router, so the client sees a reply from the wrong IP and drops it. Fix: also SNAT hairpinned traffic so the reply must go back through the router:

nft add rule ip nat postrouting ip saddr 10.0.0.0/24 ip daddr 10.0.0.5 tcp dport 8443 masquerade

Better: split-horizon DNS so internal clients get the internal IP directly.

Verification and troubleshooting

  • Outbound works, some connections drop under load — conntrack table full. dmesg shows nf_conntrack: table full, dropping packet. Raise nf_conntrack_max and lower timeouts (nf_conntrack_tcp_timeout_time_wait, ..._established).
  • Port forward not working — you added the nat rule but not the forward filter rule, or ip_forward=0, or the internal host's default gateway isn't the NAT box (so replies go elsewhere).
  • Port exhaustion behind one IP — ~64k ports per protocol per destination tuple. A few thousand clients all hitting the same destination can exhaust it. Add more public IPs (SNAT --to a.b.c.1-a.b.c.4) or reduce connection churn.
  • FTP / SIP / PPTP / IPsec broken through NAT — these embed IP addresses in the payload, which NAT doesn't rewrite. FTP needs the nf_conntrack_ftp / nf_nat_ftp helper (or passive mode); SIP needs an ALG or a proper media relay; IPsec needs NAT-T (UDP 4500).
  • Internal users can't reach the public URL of an internal service — hairpin NAT (above) or split DNS.
  • Return traffic goes to the wrong internal host — a conntrack entry collided or was flushed mid-connection. conntrack -E (events) to watch; usually a conntrack table that's too small or timeouts too short.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.