NAT was supposed to be a stopgap — a 1994 hack to stretch IPv4 until the real fix arrived. Thirty years on, it’s load-bearing infrastructure on every home router, every cloud VPC gateway, and every Kubernetes node, and its side effects (dying idle connections, unreachable peers, a kernel table that fills up during traffic spikes) are so pervasive that whole protocol families exist just to work around it. Understanding NAT means understanding the state table underneath — on Linux, conntrack.

The Rewrite, and the Memory It Requires

Your laptop (10.0.0.5) talks to a server; the router masquerades:

text
  out: 10.0.0.5:53012 → 93.184.216.34:443
       rewritten to  203.0.113.7:53012 → 93.184.216.34:443

  in:  93.184.216.34:443 → 203.0.113.7:53012
       ...which internal host? The packet alone cannot say.

The return packet is unanswerable without memory. So the router records each flow in a state table keyed by 5-tuple; every reply is matched against it and un-rewritten. (When two internal hosts pick the same source port, the NAT rewrites the port too — the table, not the packet, is the source of truth.) That’s the fundamental deal: NAT converts the stateless IP internet into stateful infrastructure, and every NAT pathology is a property of that state.

On Linux the machinery is explicit: conntrack -L dumps the live table (sudo conntrack -E streams events); entries carry protocol state and a countdown. iptables/nftables NAT rules only see a flow’s first packet — everything after follows the conntrack entry, which is why editing NAT rules doesn’t affect established flows, a fact that has confused every firewall admin at least once.

The Pathologies, Enumerated

State expires; connections don’t know. An idle TCP flow’s entry times out (established default is generous — days — but middlebox NATs and stateful clouds are far stingier, minutes to hours); UDP gets 30–180 seconds. The next packet arrives at a NAT with no memory of the flow and is dropped or RST’d — the classic “my SSH session dies overnight,” and why long-lived-connection software universally ships keepalives tuned below common NAT timeouts (SSH ServerAliveInterval, TCP keepalive, WireGuard PersistentKeepalive = 25 — that specific number is UDP NAT timeout folklore crystallized into a default).

The table is finite. nf_conntrack_max (scaled from RAM) caps entries; at the cap, new flows are dropped with the infamous dmesg line nf_conntrack: table full, dropping packet — a signature incident on busy gateways, load balancers, and Kubernetes nodes (where kube-proxy puts every service connection through conntrack). Short-lived flood traffic (DNS, health checks, port scans) fills it fastest since even closed flows linger in TIME_WAIT-ish states. Fixes in honest order: raise the max and shrink timeouts, exempt traffic that doesn’t need tracking (nftables notrack for high-rate stateless DNS), or stop NAT-ing that path entirely.

Inbound is undefined. Without an existing entry, an unsolicited inbound packet has no answer — that accidental firewall is most of a home network’s security posture. Port-forwarding (DNAT) pre-seeds the mapping; hairpin NAT covers the corner where an internal client hits its own public IP (routers that don’t support it create the “works from LTE, fails from my LAN” mystery). And two NAT’d peers who both lack inbound reachability meet via hole punching: both send outward (creating mappings), a rendezvous server swaps the observed public endpoints, and each peer’s packets thread the other’s freshly punched hole. Whether that works depends on mapping behavior — endpoint-independent NATs (“full cone”) punch easily; symmetric NATs (per-destination mappings) defeat it, forcing TURN-style relays. The entire STUN/ICE/WebRTC stack, and tailscale-style meshes, are engineering built on classifying and exploiting router state-table behavior — protocol design downstream of a hack.

CGNAT stacks the hack twice (your “public” IP is itself private, port-block-allocated across subscribers), breaking port forwarding outright and making per-IP rate limiting punish whole neighborhoods. IPv6 removes the address-scarcity reason for all of this; the stateful-firewall behavior survives by policy, hole punching and keepalives with it. NAT’s stopgap became a permanent layer of the stack, which is the most networking lesson a lesson has ever been.

Takeaways

  • NAT is a rewrite plus a state table; replies are only routable because conntrack remembers the flow — rules see first packets, state sees the rest.
  • Idle flows outlive their NAT entries: keepalives under the timeout (WireGuard’s 25 s is the canonical UDP number) are mandatory hygiene.
  • nf_conntrack table-full drops are a classic gateway/K8s incident: raise limits, cut timeouts, notrack stateless floods.
  • Unsolicited inbound needs pre-seeded state (DNAT) or hole punching; symmetric NATs defeat punching and force relays — hence STUN/ICE/TURN.
  • conntrack -L/-E makes every claim here observable on your own router.