Networking
NAT — Network Address Translation
One public IP, thousands of private hosts
NAT lets many private hosts share one public IP by rewriting source ports on outbound packets. It's why your home router works, why P2P is hard, and why STUN/TURN/ICE exist.
- Port space~64K per IP
- UDP idle timeout30 s typical
- TCP idle timeout2 hours typical
- RFC 1918 ranges10/8, 172.16/12, 192.168/16
- Defined inRFC 3022 (1994)
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
How NAT works
A NAT box sits between a private network (RFC 1918 addresses like 192.168.1.0/24) and the public internet. Every outbound packet has its source IP and source port rewritten to the NAT's own public IP and a freshly chosen port. The NAT remembers the mapping in a table — an entry like
(192.168.1.42:50001, 1.1.1.1:443) -> (203.0.113.7:62000)
says: traffic from internal host 192.168.1.42 source port 50001 going to 1.1.1.1:443 leaves as 203.0.113.7:62000. Reply packets coming back to 203.0.113.7:62000 are rewritten in reverse and delivered to 192.168.1.42:50001. The internal host believes it talks directly to the internet; the internet believes it talks to the NAT.
The translation table is keyed differently in different NAT flavors, and that key is what makes peer-to-peer traversal hard. Four classic types are defined in RFC 3489 (now superseded but still descriptively useful):
| Type | Mapping key | Filtering | P2P friendly? | Where seen |
|---|---|---|---|---|
| Full-cone | (internal IP, internal port) | None — anyone can reach the public mapping | Yes | Some old home routers |
| Restricted-cone | (internal IP, internal port) | Only allows replies from a destination IP we previously sent to | With STUN | Many home routers |
| Port-restricted | (internal IP, internal port) | Only (dest IP, dest port) we previously sent to | With STUN + hole-punching | Most consumer routers |
| Symmetric | (internal IP, internal port, dest IP, dest port) | Different external port per destination | No — needs TURN | Carrier-grade NAT, mobile |
| NAT64 | IPv6 source mapped to v4 | Stateful | One-way (v6 client → v4 server) | Mobile carriers |
| CGNAT | (subscriber, internal port, dest) | Aggressive timeouts, port exhaustion | Symmetric in practice | ISPs since IPv4 ran out |
The killer fact: in symmetric NAT, asking a STUN server "what's my public address?" gives you a port that is only valid for traffic between you and the STUN server. A peer trying to send to that port from a different IP will not match the mapping and the packet is dropped. This is why ~10% of WebRTC sessions fall back to TURN relays.
When NAT helps and when it hurts
- Helps: address conservation (one /29 of public IPs can serve thousands of customers), accidental ingress filtering (no inbound flow ⇒ no rewrite ⇒ packet dropped), and provider mobility (renumber the public side without touching internal hosts).
- Hurts: end-to-end principle (a host can't be a server without explicit port forwarding), P2P (hole-punching is a 30-line state machine for what should be a TCP connect), latency (every packet goes through extra rewrite logic), and protocols that embed addresses in the payload (FTP, SIP — they need ALG helpers that often break).
JavaScript implementation — hairpinning detection
// Detect whether the local NAT supports hairpinning by sending a UDP
// packet from one socket to OUR OWN public mapping and seeing if it
// arrives at a second socket bound to the corresponding internal port.
import dgram from 'node:dgram';
async function detectHairpin(stunServer, stunPort) {
const probe = dgram.createSocket('udp4');
const listener = dgram.createSocket('udp4');
probe.bind();
listener.bind();
// 1. Use STUN to learn our public mapping for `listener`.
const { address: pubIP, port: pubPort } = await stunBinding(
listener, stunServer, stunPort
);
console.log(`Public mapping for listener: ${pubIP}:${pubPort}`);
// 2. Send from `probe` to our own public mapping.
const seen = new Promise((resolve) => {
listener.once('message', (msg) => resolve(msg.toString()));
setTimeout(() => resolve(null), 2000);
});
probe.send('hairpin-test', pubPort, pubIP);
const result = await seen;
console.log(result
? '✓ Hairpinning supported — symmetric P2P inside LAN works'
: '✗ No hairpin — packet looped back was dropped');
probe.close();
listener.close();
}
// (stunBinding is a 30-line classic STUN BINDING request implementation.)
In practice, browsers and game clients don't ship hairpin detection — they just try the public mapping first and fall back to LAN discovery if it fails. The ICE algorithm encodes this fallback chain.
Python implementation — port mapping enumeration
"""
Probe a NAT to estimate how many concurrent UDP flows it supports
before port exhaustion. Educational tool — running against a NAT
you don't own is unfriendly.
"""
import asyncio
import socket
async def probe_one(loop, target):
transport, _ = await loop.create_datagram_endpoint(
lambda: asyncio.DatagramProtocol(),
local_addr=('0.0.0.0', 0),
remote_addr=target,
)
sock = transport.get_extra_info('socket')
src_ip, src_port = sock.getsockname()
transport.sendto(b'\\x00') # nudge to create the mapping
await asyncio.sleep(0)
return src_port, transport
async def main():
loop = asyncio.get_running_loop()
target = ('203.0.113.7', 12345)
transports = []
src_ports = set()
try:
for i in range(2000):
port, t = await probe_one(loop, target)
transports.append(t)
src_ports.add(port)
if i % 100 == 0:
print(f"opened {i:>4} flows, {len(src_ports)} unique source ports")
except OSError as e:
print(f"hit limit at {len(transports)} sockets: {e}")
finally:
for t in transports:
t.close()
asyncio.run(main())
On a typical home router this script tops out around 4,000-8,000 simultaneous UDP mappings before the table fills. CGNAT subscribers often see 1,000-2,000 — enough that a busy household running smart-TVs, phones, and laptops can hit the wall.
Concrete costs and limits
- Port space: 65,536 ports per public IP, minus ~10K reserved for the OS itself. A single subscriber on CGNAT may be capped at 2,000 ports — 10 simultaneous browser tabs each holding 50 connections (Slack, Gmail, YouTube, etc.) can exhaust that.
- Mapping lifetime: RFC 4787 recommends ≥2 minutes for UDP, 2 hours 4 minutes for established TCP. In the wild: 30 s for UDP, 5-30 min for TCP. WebRTC and SIP clients send keepalives every 15-25 s.
- Hairpin support: ~80% of consumer routers in 2026 (improved from ~50% a decade ago, but not universal).
- Symmetric NAT prevalence: ~5-15% of clients globally; higher in mobile (~25-35%) because carriers run aggressive CGNAT.
- STUN/TURN cost: STUN is one packet — negligible. TURN relays the entire media stream — for a 2 Mbps video call between two symmetric-NAT peers, the TURN server pays 2 Mbps of egress for the call's lifetime. WebRTC services budget ~10% of users behind TURN.
Variants and traversal techniques
- STUN (RFC 5389) — Session Traversal Utilities for NAT. A tiny request/response on UDP/3478 that tells you "the public IP and port your last packet was rewritten to." Foundation of every P2P stack.
- TURN (RFC 5766) — Traversal Using Relays around NAT. When STUN can't establish a direct path (symmetric NATs on both ends), a TURN server forwards traffic for both peers. Pays the bandwidth so the connection works.
- ICE (RFC 8445) — Interactive Connectivity Establishment. The decision tree that gathers candidate addresses (host, server-reflexive via STUN, relayed via TURN), tests pairs in priority order, and picks the lowest-latency working pair. Used by WebRTC and SIP.
- PCP (RFC 6887) — Port Control Protocol. Lets a host explicitly request an inbound port mapping from the NAT (replacing the legacy uPNP and NAT-PMP). When supported, removes the need for hole-punching entirely.
- Hole-punching — both peers send simultaneous outbound packets to each other's public mapping. The first packets are dropped (no inbound rule yet) but they create reciprocal mappings; subsequent packets get through. Works for full-cone, restricted-cone, and port-restricted; fails for symmetric.
- NAT64 + DNS64 — synthesizes IPv6 addresses for IPv4-only servers and translates the traffic at the carrier edge. Lets IPv6-only mobile clients reach the legacy v4 internet.
Common bugs and edge cases
- P2P on symmetric NAT. Two peers behind symmetric NATs can never hole-punch. Detection: run STUN and check whether the reflexive port equals the local port for two different STUN servers — if not, the NAT is symmetric. Fallback: TURN.
- Mapping evicted mid-call. A 90-second silence on a UDP flow can void the mapping. The next outbound packet creates a new mapping with a different external port — and the peer's keepalives now go to a stale address. Fix: keepalives every 15-25 s.
- Hairpinning fails. Two clients on the same LAN both use the public IP, but the NAT can't route a packet back inward through itself. Result: same-LAN calls drop while cross-LAN calls work. Fix: prefer host candidates first in ICE, fall back to public.
- Port exhaustion at CGNAT. A heavy user opens too many flows; the carrier rate-limits or recycles long-idle mappings. Symptom: TCP RSTs and "connection refused" on a familiar service that worked an hour earlier.
- FTP and SIP ALG bugs. Application-layer gateways try to rewrite addresses inside payloads. They botch the rewrites, corrupt the stream, or fail with TLS where the payload is opaque. Disable ALG on the router; use a modern protocol.
- NAT loopback for hosted servers. Self-hosting on the public IP often fails from inside the LAN because the router can't loop the connection. Fix: split-horizon DNS that resolves the public name to the LAN IP for internal clients.
Frequently asked questions
Why does NAT exist?
IPv4 has 4.3 billion addresses; the world has more devices than that. NAT lets a household, office, or carrier hide thousands of internal devices behind a single public IP. It was a stopgap for IPv4 exhaustion that became permanent.
What's the difference between NAT and a firewall?
NAT rewrites addresses; a firewall filters packets. Most home routers do both, which is why NAT is often described as "a firewall by accident." Inbound traffic with no matching outbound flow has nowhere to go — that's a happy side effect, not the design.
How many connections can one NAT support?
Per public IP, the theoretical ceiling is 65,535 source ports per (peer-IP, peer-port) tuple. In practice carrier-grade NATs dimension for 1,000-2,000 concurrent flows per subscriber and reuse ports across destinations using port-restricted mapping. CGNATs running tens of thousands of subscribers behind one IP are routine.
What is symmetric NAT?
A NAT that picks a different external port for every (destination IP, destination port) the host talks to. This makes hole-punching impossible from a third party because the port the peer sees isn't the same port a STUN server saw. WebRTC falls back to TURN relays in this case, paying the bandwidth cost.
What is hairpinning?
When two hosts on the same private network try to reach each other via the public IP and external port. The packet leaves the LAN, reaches the NAT, and must be rewritten to come back inside — many cheap routers fail this and the packet is dropped. STUN tests for hairpin support before deciding on a transport.
Does IPv6 eliminate NAT?
Mostly yes — every device gets a globally routable address. But carriers still deploy NAT64 to bridge IPv6 clients to IPv4-only servers, and some networks use NPT66 for prefix renumbering. End-to-end addressability comes back; address-translation logic does not fully die.