Network Address Translation (NAT)

In the early 1990s the internet faced a looming crisis. IPv4 offers only about 4.3 billion addresses. The number of internet-connected devices was growing far faster than address allocation could sustain. A home today might have fifteen devices — phones, laptops, a smart thermostat, a game console. Yet an internet service provider typically hands out just one public IPv4 address per household. Something has to reconcile the abundance of devices with the scarcity of addresses. That something is Network Address Translation. NAT is a technique, usually implemented in a home router or an enterprise gateway. It lets many devices on a private network share one or a few public IP addresses. It rewrites the source address (and often the source port) of outgoing packets. This lets replies find their way back to the correct internal device. The outside world only ever sees one address, though. This single idea single-handedly bought the internet fifteen to twenty extra years of life. That life came before IPv6: Motivation and Datagram Format became truly necessary. It remains embedded in essentially every home router, corporate firewall, and cloud VPC on Earth today. This article builds NAT from first principles. It covers what private address spaces are and why they exist. It explains how a NAT device maintains translation state. It covers the dominant scheme called Port Address Translation (PAT, or NAPT). It also details the precise mechanics of rewriting headers and recomputing checksums. It also confronts NAT's most consequential side effect head-on. NAT breaks the internet's original end-to-end connectivity model. It turns "any host can initiate a connection to any host" into "only the inside can initiate to the outside." The article surveys the workarounds (port forwarding, STUN/TURN, hole punching) that the industry built to cope. Understanding NAT is essential background for IPv6: Motivation and Datagram Format. NAT's very existence — and its various forms of breakage — is one of the strongest arguments for a larger address space. It also connects directly to IPv4 Addressing and Subnetting and CIDR and Hierarchical Address Aggregation. NAT is precisely the mechanism that lets a whole subnet of private addresses masquerade behind the aggregated public prefixes those systems allocate.

Key takeaways
  • NAT lets many devices share few public IPv4 addresses by rewriting source (IP, port) pairs in packet headers.
  • Private address ranges (RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) are reserved and never routed on the public internet.
  • Port Address Translation (PAT/NAPT) is the dominant form: one public IP, thousands of internal hosts, distinguished by port number.
  • A NAT table maps (private IP, private port, protocol) ↔ (public IP, public port); it is built dynamically as connections are made.
  • NAT breaks unsolicited inbound connections, complicates peer-to-peer protocols, and requires checksum recomputation on every rewritten header.
  • Workarounds — port forwarding, UPnP, STUN, TURN, and hole punching — exist because NAT violates the internet's original end-to-end principle.
The core insight

NAT conserves scarce public IPv4 addresses by using port numbers as a second dimension of addressing, at the cost of breaking symmetric, end-to-end reachability.

The idea

A private address is an IPv4 address drawn from one of three blocks reserved by RFC 1918 — `10.0.0.0/8`, `172.16.0.0/12`, and `192.168.0.0/16` — that routers on the public internet agree never to forward. Because these addresses are unroutable globally, they can be reused independently inside millions of separate private networks (home LANs, corporate intranets, cloud VPCs) without any risk of collision, since packets carrying them never leave their local network unmodified. This is what makes NAT possible: the private network can use whatever internal addressing scheme it wants, as long as a NAT gateway sits at the boundary and translates before packets leave.

Network Address Translation is the general term for rewriting IP header fields, at a routing boundary, to map one address realm to another. The most basic form, sometimes called static NAT or one-to-one NAT, maps a single private address to a single public address permanently — useful for a server that must be reachable from outside. Dynamic NAT maps private addresses to a pool of public addresses on demand, still one-to-one at any instant. Neither of these actually solves the address-scarcity problem, because they still require roughly one public address per simultaneously active private host. The scheme that actually matters in practice is Port Address Translation (PAT), also called Network Address and Port Translation (NAPT), or simply "NAT" in colloquial usage (this is what your home router does). PAT exploits the fact that a TCP or UDP connection is identified by a 4-tuple — source IP, source port, destination IP, destination port — plus the protocol. Since port numbers are 16 bits, a single public IP address can in principle support up to 65,536 simultaneously distinguishable port numbers, so if the NAT gateway rewrites not just the source IP but also the source port of every outgoing packet, thousands of internal hosts can share one public IP, distinguished purely by which port the gateway assigned to their connection. This is the "many-to-one" translation everyone actually means by "NAT."

How it works

The NAT gateway sits at the boundary between the private network (the "inside") and the public internet (the "outside"). It maintains a translation table, sometimes called a NAT table or connection table, that records the mapping between internal and external endpoints for every active flow. Each entry is created the first time an internal host sends an outbound packet, and is torn down after the flow ends or an idle timeout expires.

Outbound path (private host → internet): 1. An internal host with private address `10.0.0.5` opens a TCP connection to a public server `203.0.113.9:80`, using ephemeral source port `40001`. 2. The packet's IP/TCP header reads: `src = 10.0.0.5:40001`, `dst = 203.0.113.9:80`. 3. The packet reaches the NAT gateway. The gateway checks its table for an existing entry for `(10.0.0.5, 40001, TCP)`. None exists, so it creates one, choosing a free public port — say `61001` — on its own public IP `198.51.100.7`. 4. The gateway rewrites the header: `src = 198.51.100.7:61001`, recomputes the IP and TCP checksums (since the source address/port changed), and forwards the packet. 5. The server sees a connection apparently from `198.51.100.7:61001` and has no visibility into the private network behind it.

Inbound path (reply → private host): 1. The server replies to `198.51.100.7:61001`. 2. The gateway receives the packet, looks up `(198.51.100.7, 61001, TCP)` in its table (indexed by the external endpoint this time), finds the mapping back to `(10.0.0.5, 40001)`, rewrites the destination header accordingly, recomputes checksums, and forwards it into the private network. 3. Crucially, a packet arriving unsolicited at `198.51.100.7:61001` with no matching table entry is simply dropped — there is no private host to deliver it to, and the gateway has no way to guess one. This is the source of NAT's connectivity restrictions, discussed below. ``` // NAT gateway state: a table of active translations. // Keyed by (protocol, direction-specific tuple). table : map from Key to Entry struct Entry: privateIP, privatePort publicIP, publicPort protocol // TCP, UDP, or ICMP-query-id lastUsed // timestamp, for idle eviction function TRANSLATE_OUTBOUND(packet): key_in = (packet.protocol, packet.srcIP, packet.srcPort, packet.dstIP, packet.dstPort) if key_in in table: entry = table[key_in] else: // Allocate a fresh public port not currently in use // on this public IP for this protocol. pubPort = ALLOCATE_FREE_PORT(publicIP, packet.protocol) if pubPort is NULL: DROP(packet) // port pool exhausted return entry = new Entry(packet.srcIP, packet.srcPort, publicIP, pubPort, packet.protocol) table[key_in] = entry table[reverseKey(entry)] = entry // index for return traffic entry.lastUsed = NOW() packet.srcIP = entry.publicIP packet.srcPort = entry.publicPort RECOMPUTE_CHECKSUMS(packet) // IP header + TCP/UDP pseudo-header FORWARD(packet, outInterface) function TRANSLATE_INBOUND(packet): key_out = (packet.protocol, packet.dstIP, packet.dstPort, packet.srcIP, packet.srcPort) if key_out not in table: DROP(packet) // unsolicited inbound: no mapping return entry = table[key_out] entry.lastUsed = NOW() packet.dstIP = entry.privateIP packet.dstPort = entry.privatePort RECOMPUTE_CHECKSUMS(packet) FORWARD(packet, inInterface) function GARBAGE_COLLECT(): for entry in table.values(): idle = NOW() - entry.lastUsed limit = (entry.protocol == TCP) ? TCP_IDLE_TIMEOUT // e.g. hours, or on FIN/RST : UDP_IDLE_TIMEOUT // e.g. 30-300 seconds if idle > limit: remove table[entry.forwardKey] remove table[entry.reverseKey] ``` Some important refinements the pseudocode above glosses over:

  • TCP connections are usually torn down promptly on seeing a `FIN`/`FIN-ACK` exchange or an `RST`, rather than waiting for a generic idle timeout, so port numbers are recycled quickly.
  • UDP has no connection state at the protocol level, so the NAT gateway must infer "flow" boundaries purely from idle time, since there is no FIN to observe. This is why long-lived UDP applications (VoIP, some games) send periodic keepalive packets — to refresh the NAT table entry before it is garbage collected.
  • ICMP echo (ping) is handled specially: since ICMP has no port field, NAT implementations typically translate the ICMP query identifier field the same way they would translate a port.
  • Checksum recomputation is mandatory correctness work, not optional bookkeeping: both the IP header checksum and the TCP/UDP checksum (which covers a pseudo-header including the IP addresses) depend on the addresses/ports that NAT just changed. A NAT device that forwards a rewritten packet without fixing checksums produces a packet the receiver's kernel will silently discard as corrupt.
Figure
Port Address Translation: one public IP serving three private hosts via the NAT table
Private network (10.0.0.0/24) Host A 10.0.0.5 : 40001 Host B 10.0.0.6 : 51500 Host C 10.0.0.7 : 40001 NAT Gateway 198.51.100.7 (public IP) NAT translation table A 10.0.0.5:40001 → :61001 B 10.0.0.6:51500 → :61002 C 10.0.0.7:40001 → :61003 note: A and C share the same private port (40001) but map to distinct public ports Public internet Server 203.0.113.9:80 src=198.51.100.7:61001 (after rewrite)

Complexity & correctness

Time complexity. Per-packet processing at the NAT gateway is `O(1)` expected time: a hash-table lookup on the 5-tuple (or 4-tuple/id for ICMP), a possible insertion on the first packet of a flow, an update of the `lastUsed` timestamp, and a fixed number of arithmetic operations to recompute the IP and TCP/UDP checksums (checksum recomputation can even be done incrementally — RFC 1624 gives an `O(1)` update formula that adjusts the old checksum by the delta between old and new address/port fields, rather than resumming the whole packet). This is essential: a NAT gateway sits on the forwarding path of every packet in the network, so anything worse than `O(1)` per packet would not scale to line rate.

Space complexity. The table holds `O(k)` entries, where `k` is the number of concurrent flows currently traversing the gateway — bounded above by the number of available (public IP, port, protocol) triples, i.e. at most roughly `65,536 × (number of public IPs)` per protocol. This is the fundamental resource constraint of PAT: with one public IP, a NAT gateway can support on the order of 64K concurrent flows per protocol (fewer in practice, since well-known ports below 1024 are typically reserved and some port ranges are used for other purposes), which is why carrier-grade NAT deployments serving thousands of subscribers behind one IP must either use multiple public IPs or carefully partition the port space per subscriber.

Correctness argument. The core invariant NAT must preserve is: > For every packet that the gateway forwards inbound, there exists a unique, previously-created table entry whose external key exactly matches the packet's (protocol, destination IP, destination port, source IP, source port) — and the corresponding internal endpoint is the one, and only the one, private host that could have originated the flow this reply belongs to. This invariant holds because: 1. Uniqueness of allocation. `ALLOCATE_FREE_PORT` only ever hands out a public port that is not currently associated with another active entry for the same public IP and protocol, so at any instant the mapping from (public IP, public port, protocol) to (private IP, private port) is a well-defined function — never one public endpoint serving two different private flows simultaneously. 2. Bidirectional table population. Every outbound translation installs both the forward key (indexed by private tuple, used for further outbound packets of the same flow) and the reverse key (indexed by public tuple, used to route the eventual reply) atomically, in the same table-insertion step. So by the time the first outbound packet leaves the gateway, the return path is already correctly primed. 3. Symmetric NAT policy (used by most consumer-grade NAT implementations) additionally requires that only packets from the same remote endpoint the internal host originally contacted are accepted back through a given mapping — this is what makes NAT double as a rudimentary stateful firewall: an attacker who doesn't know the internal 4-tuple, and isn't the exact remote host originally contacted, cannot inject packets that match an existing entry. 4. Garbage collection is safe because it only removes entries once idle beyond the protocol's timeout (or after observing TCP connection teardown), and any packet arriving on a removed mapping is legitimately unroutable — the "flow," from the gateway's point of view, no longer exists, matching the semantics that TCP/UDP applications above it already assume (a connection that's been idle past its keepalive interval, or explicitly closed, is gone).

Tradeoffs, honestly stated. This correctness argument is exactly why NAT breaks unsolicited inbound connectivity: the invariant requires a prior outbound packet to have created the mapping. There is no way, by design, for an external host to be the first to speak to an internal host through PAT — this is not a bug to be fixed but the direct logical consequence of the mechanism that makes address sharing possible at all. Any application that needs an external party to initiate contact (a peer in a P2P file-sharing swarm, a video-call peer for direct media, a home server) must use one of the workarounds discussed below, precisely because the invariant above cannot be relaxed without also breaking NAT's ability to conserve address space and act as an implicit firewall.

Worked example

Consider a small office with three internal hosts behind one NAT gateway with public IP `198.51.100.7`. Trace their traffic and the resulting NAT table over time.

TimeEventPrivate tupleActionPublic tuple assignedTable size after
t=0Host A opens TCP to `203.0.113.9:80``10.0.0.5:40001`No entry found → allocate`198.51.100.7:61001`1
t=1Host B opens TCP to `203.0.113.9:80``10.0.0.6:51500`No entry found → allocate`198.51.100.7:61002`2
t=2Host C opens TCP to `93.184.216.34:443` — note same private port as A (40001)`10.0.0.7:40001`No entry found → allocate (private ports may collide across hosts; public ports never do)`198.51.100.7:61003`3
t=3Server `203.0.113.9` replies to `198.51.100.7:61001`—Lookup reverse key → found, routes to `10.0.0.5:40001`(reused)3
t=4Host A sends a second packet on the same connection`10.0.0.5:40001`Forward key already exists → reuse `61001`, refresh `lastUsed`(reused)3
t=5Host A's connection closes (FIN/ACK observed)—Entry marked for prompt removal rather than waiting for idle timeout—2
t=6An unsolicited packet arrives at `198.51.100.7:61001` from a scanner probing the internet—No reverse-key entry (removed at t=5) → dropped—2
t=7Host B's UDP-based application goes idle for 400 seconds (timeout is 300s)`10.0.0.6:51500`Entry expires and is garbage collected—1

Two points this trace makes concrete. First, at `t=2`, Host A and Host C use the identical private source port (`40001`) simultaneously — this is completely fine, because NAT disambiguates using the full 4-tuple including private IP, and moreover assigns them different public ports (`61001` vs `61003`), so there is never any ambiguity on the public side either. Second, at `t=6`, the scanner's packet is dropped even though it targets a port number that was recently valid — this is the direct, correct consequence of the invariant argued above: no live table entry, no delivery, exactly as intended, both for correctness (there's no host to deliver to) and for security (unsolicited external traffic simply cannot reach internal hosts by default).

Common pitfalls & traps

PitfallWhy it happensConsequence
Assuming an internal server is reachable from outside "for free"NAT only creates mappings from outbound traffic; nothing primes a mapping for a server that never initiates connectionsInbound connection attempts to the server are silently dropped until a static port-forwarding rule is configured
Forgetting to recompute checksums after rewriting addresses/portsTreating NAT as "just changing the address" without touching the TCP/UDP pseudo-header checksumReceiving hosts discard the packet as corrupted; intermittent, hard-to-diagnose connectivity failures
Relying on a fixed public port across restarts for a P2P appAssuming the NAT mapping is stable, when many implementations (symmetric NAT) allocate a new public port for every new destinationPeer-to-peer "hole punching" fails intermittently depending on NAT vendor/behavior
Long-lived UDP flows dying silentlyUDP has no FIN, so NAT can only infer flow end via idle timeout, often 30–300 secondsVoIP calls or game sessions drop mid-use unless the application sends keepalives
Treating carrier-grade NAT port exhaustion as a client bugAssuming per-IP public port space (~64K) is unlimited when thousands of subscribers share one IP behind an ISP's CGNATNew outbound connections silently fail once the shared port pool is exhausted, and it looks like "flaky internet" rather than a capacity limit
Double NAT confusion (router behind ISP's own NAT)Two independent translation layers, each maintaining separate tables, with no coordinationPort forwarding configured on the home router alone is insufficient; inbound reachability additionally requires ISP-side configuration that home users usually cannot set

Pioneers & history

NAT emerged not from a single celebrated inventor but from a pragmatic, engineering-driven response to imminent address exhaustion, formalized in the Internet Engineering Task Force's RFC process during the early-to-mid 1990s.

Origins · 1 minKjeld Borch Egevang & Paul Francis and how this was discovered
KB
Kjeld Borch Egevang & Paul Francis
active 1990s · co-authors of RFC 1631, "The IP Network Address Translator (NAT)," May 1994

Egevang and Francis formalized NAT as an Internet Standards Track proposal at a moment when projections suggested IPv4 address space could be exhausted within a few years absent intervention. Their RFC framed NAT explicitly as a short-term stopgap, buying time for the deployment of a larger address architecture — what would become IPv6: Motivation and Datagram Format — rather than as a permanent architectural feature. In practice, NAT proved so operationally convenient (and doubled so effectively as a lightweight firewall) that it outlived its intended "short-term" framing by decades and became a permanent fixture of nearly every consumer and enterprise network.

History
1994
— RFC 1631 formally proposes NAT as an interim solution to IPv4 address exhaustion.
1996
— RFC 1918 reserves the private address blocks (10/8, 172.16/12, 192.168/16) that NAT depends on.
2001
— RFC 3022 formalizes "Traditional NAT," distinguishing Basic NAT from Network Address Port Translation (NAPT).
2003–2008
— STUN, then ICE and TURN, are standardized to let real-time applications (VoIP, video calls) traverse NAT reliably.
2011
–present — IANA exhausts its IPv4 free pool; carrier-grade NAT (CGNAT) becomes common as ISPs share single public IPs across many subscribers, intensifying pressure toward IPv6 adoption.

Where this matters

Every consumer home router performs NAT by default, translating the dozens of devices on a home LAN behind a single ISP-assigned IPv4 address — this is the single most common instance of the mechanism in daily life. Enterprise firewalls and cloud provider gateways (AWS NAT Gateway, Azure NAT Gateway, GCP Cloud NAT) apply the identical PAT logic so that entire private subnets of servers can reach the internet for updates and API calls without each server consuming a scarce public IP. Mobile carriers deploy carrier-grade NAT (CGNAT), translating an entire cellular network's traffic behind a small pool of public addresses, which is why techniques like STUN-based NAT traversal are essential for any mobile app offering direct voice or video calling. Real-time communication stacks (WebRTC, SIP-based VoIP, online multiplayer games) build entire connectivity subsystems — STUN to discover one's own public mapping, TURN to relay when direct traversal fails, ICE to coordinate candidate address negotiation between two NAT-bound peers — purely to route around the end-to-end connectivity NAT removes. Understanding NAT is also the natural bridge to appreciating why IPv6: Motivation and Datagram Format restores direct end-to-end addressability: with a vastly larger address space, the entire motivation for translating and sharing addresses — and the connectivity compromises that come with it — disappears.

Ask the tutor: How does NAT64 let an IPv6-only client communicate with an IPv4-only server, and where does it fit alongside ordinary PAT?

Is NAT the same thing as a firewall?

Not exactly, though they overlap heavily in practice. NAT's core job is address translation: rewriting (IP, port) pairs so many private hosts can share fewer public addresses. A firewall's job is policy enforcement: deciding which packets are allowed through based on rules. However, because NAT's translation table only accepts inbound packets that match a mapping created by a prior outbound packet, NAT incidentally behaves like a default-deny firewall for unsolicited inbound traffic — this is why home routers with NAT enabled feel reasonably safe even with no explicit firewall rules configured. Dedicated firewalls add much richer capability: deep packet inspection, stateful rule sets beyond simple flow matching, and application-layer filtering, which NAT alone does not provide.

Why do video calls sometimes fail to connect directly between two home networks?

Both callers are typically behind NAT gateways, and NAT's correctness invariant requires an outbound packet to seed any inbound mapping — so neither side's gateway will accept the other's very first packet as "unsolicited." Protocols like ICE coordinate both endpoints to send packets to each other at nearly the same time (UDP hole punching), which works often but not always, especially against "symmetric" NAT implementations that allocate a fresh public port for every distinct destination a host contacts. When hole punching fails, the call falls back to a TURN relay server that both sides can reach outbound, at the cost of extra latency and bandwidth, since all media now traverses a third party instead of going directly between the two callers.

Can two different NAT table entries ever collide and misroute a packet?

No, as long as the NAT implementation correctly enforces uniqueness at allocation time. The gateway's `ALLOCATE_FREE_PORT` step only hands out a public port that is not currently in use for that public IP and protocol, so the mapping from (public IP, public port, protocol) to a private endpoint is always a well-defined function at any given instant. Collisions across private addresses using the same private port (as in the worked example, Hosts A and C both using port 40001) are fine, because the private tuple includes the private IP, and the corresponding public ports assigned are guaranteed distinct. Correctness would only break if two implementations independently and carelessly reused a port without checking the table, which well-designed NAT software avoids by construction.

Does NAT slow down network traffic?

In principle, minimally. Per-packet NAT processing is O(1): one hash-table lookup plus an incremental checksum update, both of which modern router hardware and software perform at line rate with negligible added latency. In practice, most measurable "NAT slowdowns" trace back to secondary effects rather than the translation itself: carrier-grade NAT devices under heavy subscriber load can experience port-allocation contention, translation tables can become a memory bottleneck at very large scale (millions of concurrent flows), and consumer routers with underpowered CPUs can bottleneck on NAT plus simultaneous firewall/QoS processing. The translation algorithm itself is not the source of meaningful overhead; the hardware running it usually is.

Why can't a home user just configure port forwarding once and be done with reachability problems forever?

Port forwarding solves the "an external host wants to reach my internal server" problem only when there is exactly one layer of NAT and the user controls its configuration. Many ISPs deploy carrier-grade NAT (CGNAT) in addition to the home router's own NAT — a "double NAT" scenario — so the home router's port-forwarding rule only affects the first hop, and the ISP-side NAT still drops the inbound connection before it ever reaches the home router at all, because there is no matching mapping there either. Fixing this generally requires either an ISP-assigned public IP (sometimes available on request or with a business plan), a VPN or tunneling relay, or protocols like UPnP/PCP that automate port-forwarding requests, none of which help against CGNAT the user doesn't control.

Is NAT still relevant now that IPv6 exists?

Extremely relevant, because IPv4 remains the dominant addressing scheme for a large share of internet traffic even as IPv6 deployment grows, and dual-stack transition mechanisms mean NAT-adjacent techniques (like NAT64, which lets IPv6-only clients reach IPv4-only servers) are still actively used. Even in pure IPv6 deployments, some networks apply NAT-like translation for policy or privacy reasons, though IPv6's enormous address space removes the scarcity motivation that originally drove NAT's adoption. As long as IPv4 legacy infrastructure persists — which will likely be for many more years — understanding NAT remains essential to understanding how the real, heterogeneous internet actually routes traffic.

What is the difference between static NAT, dynamic NAT, and PAT/NAPT?

Static NAT maps one specific private address to one specific public address permanently, useful for a server that must always be reachable at a known public IP. Dynamic NAT maps private addresses to public addresses from a pool, still strictly one-to-one at any given moment, but the specific pairing can change over time as addresses are allocated and released. PAT (Port Address Translation), also called NAPT, is the many-to-one scheme described throughout this article: it adds port-number rewriting so that a single public IP can support thousands of simultaneous private hosts, which is what makes it the only one of the three that actually solves address scarcity at meaningful scale — static and dynamic NAT both still need roughly one public address per concurrently active private host.

Before this

Where this leads