How NAT Actually Works
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:
:80on 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.
dmesgshowsnf_conntrack: table full, dropping packet. Raisenf_conntrack_maxand lower timeouts (nf_conntrack_tcp_timeout_time_wait,..._established). - Port forward not working — you added the
natrule but not theforwardfilter rule, orip_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_ftphelper (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.
Related tools and reading¶
- On-site: Port Checker, IP Lookup, Subnet Calculator.
- Related posts: nftables for iptables users, The TCP three-way handshake.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.