§ 3.1  Module 3 — The Broken Internet: NAT and the Art of Hole Punching

How the Internet Ran Out of Addresses

The two most compelling problems facing the IP Internet are IP address depletion and scaling in routing.— RFC 1631, May 1994

By the end of this lesson

  • Explain NAT mapping and filtering behaviors using RFC 4787 terminology (EIM/ADM/APDM × EIF/ADF/APDF)
  • Implement a NAT simulator with configurable behavior policies
  • Predict which inbound packets survive under each behavior combination
  • Describe CGNAT and hairpinning and their production consequences

Historical note

IPv4's 32-bit address (RFC 791, September 1981) allows 4,294,967,296 hosts, and in 1981 that looked inexhaustible. By 1990 it did not: at that year's IETF meetings, Frank Solensky's allocation projections showed the coveted class B blocks running out around 1994, and the backbone routing tables growing past what router memory could hold. The IETF convened the ROAD (Routing and Addressing) group in late 1991 and produced two stopgaps: CIDR (RFC 1519, September 1993 — Vince Fuller, Tony Li, Jessica Yu, Kannan Varadhan), which replaced address classes with arbitrary prefix lengths, and private address space (RFC 1597, March 1994, revised as RFC 1918 in February 1996), which let any number of networks reuse the same addresses so long as they stayed off the public internet. The device that let those private hosts reach the public internet anyway — the network address translator, described by Paul Francis (then publishing as Paul Tsuchiya) and Tony Eng in a January 1993 paper and standardized by Kjeld Egevang and Francis as RFC 1631 in May 1994 — was offered explicitly as a short-term measure until a bigger address space arrived. The bigger address space arrived (IPv6, RFC 1883, December 1995) and the short-term measure outlived it as the architecture of the internet's edge: when IANA handed the last five /8 blocks to the regional registries on February 3, 2011, virtually every consumer device on earth already sat behind a NAT. The price was the founding assumption of every peer-to-peer protocol ever designed — that if I know your address, I can reach you. Skype (Niklas Zennström and Janus Friis, August 2003) proved the assumption could be restored by trickery from behind hotel routers, but proprietarily; the open standardization of that folklore became STUN, TURN, and ICE, the machinery of the rest of this module. This lesson studies the villain those tools were built to defeat.

3.1.1Thirty-two bits and the arithmetic of panic

The original allocation scheme made exhaustion arrive early. Classful addressing divided the 32 bits at fixed boundaries: a class A network (a /8 in modern notation) held 16,777,214 hosts, a class B (/16) held 65,534, a class C (/24) held 254. There were only 16,384 possible class B networks, and class B was the block everyone wanted — a university with three thousand hosts could not fit in a class C and could not justify a class A, so it took a class B and wasted 95 percent of it. This is the "three sizes fit nobody" problem, and it burned through the B space at a rate that made Solensky's 1994 exhaustion projection look conservative. The parallel crisis was routing: every class C network handed out in lieu of a B added its own route to every backbone router's table, and in the early 1990s those tables were doubling roughly annually against routers with single-digit megabytes of memory.

CIDR — Classless Inter-Domain Routing — attacked both problems by abolishing the classes. An allocation could now be any power-of-two size (192.0.2.0/23, 10.0.0.0/13), and contiguous blocks could be aggregated: a provider holding 198.51.0.0/16 announces one route no matter how many customers it carves the block into. CIDR bought roughly a decade and a half. It did not manufacture addresses, and the arithmetic stayed grim: one 32-bit space, a human population then approaching six billion, and an industry about to put a networked computer in every pocket. IPv6 fixed the arithmetic with 128 bits, but it required touching every router, every operating system, and every application — so deployment crawled while a cheaper fix spread on its own.

3.1.2RFC 1918 and the translator

The cheaper fix has two halves. RFC 1918 reserves three blocks that will never be routed on the public internet — 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 — so any site can number its interior freely without asking a registry. And RFC 1631's translator sits at the border, rewriting packets so that interior hosts can still speak to the exterior. The form that conquered the world is NAPT — Network Address Port Translation, specified in RFC 3022 (Pyda Srisuresh and Kjeld Egevang, January 2001) — which multiplexes an entire private network onto one public address by translating the port as well as the IP.

The mechanics fit in a table. When your laptop at 192.168.1.7 sends a UDP datagram from port 51472 to a server at 198.51.100.9:3478, the NAT allocates a public source — say 203.0.113.5:40001 — rewrites the packet's source IP and port (fixing the IP and UDP checksums as it goes), forwards it, and records the binding:

// One row of NAPT state, created by an outbound packet
{
  inside:   "192.168.1.7:51472",   // who opened it
  outside:  "203.0.113.5:40001",   // the public face it was given
  seenDst:  ["198.51.100.9:3478"], // where it has sent so far
  expires:  "T+300s"               // refreshed by traffic, else reaped
}

Listing 3.1 — A NAT mapping. Everything in this module turns on two policy questions this row leaves open.

When the server replies to 203.0.113.5:40001, the NAT looks up the row and rewrites the destination back to 192.168.1.7:51472. For an outbound-only client the illusion is perfect, which is why NAT spread without anyone's permission. The mapping is soft state: for UDP, RFC 4787 requires it survive at least two minutes of silence and recommends five, which is why every VoIP stack you will ever read sends keepalives.

laptop (inside) 192.168.1.7:51472 NAT 203.0.113.5 public server 198.51.100.9:3478 src rewritten → :40001 reply to 203.0.113.5:40001 → translated back inside mapping table 192.168.1.7:51472 ⇄ 203.0.113.5:40001 sent to: 198.51.100.9:3478 expires: T+300s Two open questions: reuse this row for a new destination? admit which inbound sources?
Figure 3.1 — NAPT in one round trip. The translation itself is trivial; the policies governing the table row are what make or break peer-to-peer connectivity.

Two questions decide everything that follows, and RFC 1631 answered neither, so every vendor answered them differently. First: when the same internal socket sends to a new destination, does the NAT reuse the row — keep presenting 203.0.113.5:40001 to the world — or allocate a fresh one? That is the NAT's mapping behavior. Second: once the row exists, who is allowed to send packets inward through it — anyone who knows the public address, only hosts the laptop has already contacted, or only the exact address-and-port pairs it contacted? That is the NAT's filtering behavior. They sound similar. They are independent axes, enforced at different moments: mapping is decided on the way out, filtering on the way in.

Check your understanding

Auto-graded check

2 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m3-l1-check1)

Interactive 3D instrument

How the internet ran out of addresses

A 3D instrument you drive yourself, one variable at a time. It needs JavaScript and WebGL, so it is not shown in this static copy of the page.

A server-reflexive candidate is not proof of a public addressdeployment

On carrier-grade NAT — increasingly the default on mobile and on some residential fibre — your srflx candidate's address is itself inside 100.64.0.0/10 (RFC 6598) or another private range, because the STUN server sat behind the same CGNAT tier or because a second layer of NAT exists above it. The candidate looks perfectly normal in your logs, and it is completely unroutable from outside the carrier. Two users on the same carrier can sometimes reach each other; a user on that carrier and one anywhere else cannot.

What to do

Never treat the presence of a srflx candidate as evidence of reachability. Check whether the srflx address is in 10/8, 172.16/12, 192.168/16, or 100.64/10 and downgrade your expectations accordingly, and always have TURN available. The only signal that matters is candidate-pair.state === 'succeeded'.

3.1.3Mapping and filtering: the RFC 4787 taxonomy

The IETF's BEHAVE working group finally named the axes in RFC 4787 (François Audet and Cullen Jennings, January 2007, BCP 127), and its vocabulary is the one this course uses everywhere. Each axis has three settings, from most permissive to least:

Mapping behavior — when does an internal socket get a new public port? Endpoint-independent mapping (EIM): never; one internal socket maps to one public port, reused for every destination. Address-dependent mapping (ADM): a new mapping per destination IP. Address-and-port-dependent mapping (APDM): a new mapping per destination IP and port. RFC 4787's REQ-1 makes EIM mandatory for compliant NATs — the working group understood precisely that the other two behaviors are what break traversal.

Filtering behavior — which inbound packets may use an existing mapping? Endpoint-independent filtering (EIF): any external host may send to the public port. Address-dependent filtering (ADF): only IPs the internal socket has already sent to, from any of their ports. Address-and-port-dependent filtering (APDF): only the exact IP:port pairs already contacted.

Why mapping behavior is destiny: the whole strategy of NAT traversal (next lesson) is to ask a public STUN server "what public address do my packets appear from?" and hand that reflexive address to a peer. Under EIM the answer is reusable — the same public port faces the peer as faced the STUN server. Under APDM the answer is a lie for any destination except the STUN server itself: the moment you send toward the peer, the NAT mints a different port, and the address your peer is aiming at goes nowhere. Filtering behavior then decides whether the peer's packets survive arrival; it can be defeated by sending outbound first — the "hole punch" this module is named for — but only if the packets arrive from an address and port the filter recognizes.

filtering behavior (decided on the way in) EIF ADF APDF mapping (way out) EIM ADM APDM full cone restricted cone port-restricted cone unnamed unnamed unnamed unnamed unnamed symmetric The legacy taxonomy names four cells of a nine-cell space — and hides the fact that the axes are independent.
Figure 3.2 — RFC 4787's two axes, with the RFC 3489 legacy labels overlaid. "Symmetric" is the solid cell: per-destination mapping and the strictest filter.

3.1.4The legacy labels, and why symmetric is the villain

Before RFC 4787, the industry classified NATs with a four-way taxonomy from the original STUN specification, RFC 3489 (Jonathan Rosenberg, Joel Weinberger, Christian Huitema, Rohan Mahy, March 2003). You will meet these names in every blog post, interview, and vendor datasheet, so you must be bilingual:

Table 3.1 — The RFC 3489 legacy taxonomy translated into RFC 4787 behaviors.
Legacy label (RFC 3489)MappingFilteringInbound packet admitted when…
Full coneEIMEIFanyone sends to the public port
Restricted coneEIMADFsource IP was previously contacted (any port)
Port-restricted coneEIMAPDFsource IP and port were previously contacted
SymmetricAPDMAPDFsource is the exact destination of that per-destination mapping

The taxonomy is inadequate in three ways, which is why RFC 5389 (October 2008) deleted the classification algorithm when it revised STUN. It names four of nine possible behavior combinations — an EIM/ADF-mapping box with endpoint-independent filtering has no legacy name at all. It presents mapping and filtering as one fused property, so "restricted cone versus symmetric" sounds like a difference of strictness when it is really a difference on two axes at once. And it implies a NAT is one type, when measured devices change behavior under port pressure, apply different policies to different protocols, and sometimes preserve the internal port when free (port preservation) but not otherwise. Classify behaviors, not boxes.

Still, the legacy names earn their keep in one place: predicting whether two peers can punch a direct UDP path. The punch works like this. Each peer asks a STUN server for its reflexive address and sends it to the other via signaling. Both then fire datagrams at each other's reflexive address. Each outbound datagram does double duty — it creates (or reuses) a mapping, and it teaches the sender's filter to admit the peer. Whether the punch lands depends on the pairing:

Table 3.2 — Direct UDP traversal by behavior pairing (peer replies to the source address it observes; no port prediction).
Full coneRestrictedPort-restrictedSymmetric
Full coneconnectsconnectsconnectsconnects
Restrictedconnectsconnectsconnectsconnects
Port-restrictedconnectsconnectsconnectsfails
Symmetricconnectsconnectsfailsfails

Read the failing cells with the axes in mind. Symmetric–symmetric: each side's packets leave from a fresh per-destination port the other side has never heard of, and each side's filter admits only the stale STUN-facing address — nothing matches, ever. Symmetric–port-restricted is the trap case, because "port-restricted cone" sounds tame: the symmetric side sends from a brand-new port, so its packets arrive at the port-restricted NAT from an address:port the inside host never contacted — dropped by APDF. Meanwhile the port-restricted side aims at the symmetric side's STUN-learned mapping, whose filter expects only the STUN server — also dropped. Deadlock. But make the second side merely restricted (ADF) and the punch lands: the symmetric side's new port still comes from the same public IP, and an address-only filter waves it through; the restricted side then learns the true port from the packet it just received and re-aims. That asymmetry — one axis relaxed one notch — is the difference between a call and a relay bill.

A common error

"Symmetric NAT kills peer-to-peer, period." One-sided symmetric usually connects: against full-cone or restricted-cone peers the punch succeeds, because the peer's filter checks (at most) the source address, and the symmetric NAT's surprise port still comes from the same IP. Direct traversal reliably dies only when address-and-port-dependent mapping on one side meets address-and-port-dependent filtering (or the same mapping behavior) on the other. That is exactly why ICE never assumes: it tries every pairing and keeps a relayed path in reserve — and why production services still see roughly one call in ten needing TURN.

Check your understanding

Auto-graded check

2 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m3-l1-check2)

Interactive minigame

What kind of NAT is this? — measuring mapping and filtering

A 3D instrument you drive yourself, one variable at a time. It needs JavaScript and WebGL, so it is not shown in this static copy of the page.

NAT mappings expire faster than the RFC says, and mobile suspends your timersdeployment

RFC 4787 REQ-5 asks for a 2-minute minimum UDP mapping timeout; consumer CPE routinely uses 30–60 s. WebRTC survives this because STUN consent freshness (RFC 7675) sends a binding request roughly every 5 s, which doubles as a keepalive — even on a data-channel-only connection with no media. The failure mode is mobile: when iOS Safari backgrounds the tab, JavaScript timers and often the whole networking stack are suspended, consent checks stop, and the mapping dies in under a minute. The user returns to a connection that ICE reports as disconnected or failed.

What to do

Listen for visibilitychange and, on resume, check pc.iceConnectionState and call pc.restartIce() if it is not connected. Do not rely on automatic recovery on iOS. For long-lived data channels, consider tearing down on background and reconnecting on foreground rather than fighting the OS.

3.1.5CGNAT, hairpinning, and the end of end-to-end

Address scarcity eventually pushed NAT one level up. Under carrier-grade NAT, your home router's "public" side receives a private address from the ISP — drawn from 100.64.0.0/10, the shared address space RFC 6598 reserved for exactly this in April 2012 — and a second translator inside the carrier maps thousands of subscribers onto each genuinely public IP. RFC 6888 (April 2013) sets behavioral rules for these boxes, including per-subscriber port quotas. The production consequences are real: you traverse two mapping/filtering policies in series and only control one of them; your public reputation (rate limits, abuse blocklists, geolocation) is shared with strangers; port quotas can starve a busy household mid-call; and mobile networks, where CGNAT is near-universal, are where your traversal statistics go to die. When both peers sit behind the same carrier NAT, a direct path may exist inside the carrier — if the CGN cooperates.

That cooperation has a name: hairpinning. Two hosts behind the same NAT, each knowing only the other's external mapping, need the NAT to accept a packet from inside, aimed at its own public address, and bend it back inside — traffic that enters and leaves through the same interface like a hairpin turn. RFC 4787 requires support, presenting the translated packet as coming from the mapping's external address and port; consumer routers historically failed at it, which is one concrete reason ICE always tries peers' host (private) addresses first rather than trusting reflexive ones to work between LAN neighbors.

Step back and the loss comes into focus. The internet's design ethos — argued in Jerome Saltzer, David Reed, and David Clark's 1984 paper "End-to-End Arguments in System Design" — put intelligence at the endpoints and kept the network a dumb datagram mover, which is precisely what let two graduate students deploy a new protocol without asking anyone. NAT quietly repealed that: the network's edge now holds per-flow state, silently distinguishes "client" from "server," and makes some pairs of hosts unreachable to each other by default. Nothing in this module — not STUN's address discovery, not TURN's relays, not ICE's candidate gymnastics — adds a feature a 1985 endpoint didn't have. It is all restoration work: the most intricate distributed state machine in WebRTC, built to reopen a socket the translator closed. The next lesson begins the restoration with the simplest possible question to a stranger: what address do you see me as?

Problems for § 3.1Check your understanding

Build the module's villain with your own hands, then prove you can predict its behavior from the two axes alone.

Lab 1

Build the villain: a NAT simulator

Graded in the browser against 5 assertions; the editor and harness require JavaScript.

Check your understanding

Auto-graded check

5 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m3-l1-ex2)

Field quirks

What the specifications say, and what the browsers actually do, are two different documents. These are the divergences that cost production teams their weekends.

mDNS obfuscation plus blocked multicast makes same-LAN peers relaybrowser

Chrome replaces host candidates with <uuid>.local names to avoid leaking private IPs, and the remote resolves them over mDNS. Most enterprise and guest Wi-Fi blocks multicast between clients, so the name never resolves. The two peers then fall back to srflx, which requires the router to support hairpinning (RFC 4787 REQ-9) — many do not. The result is two laptops on the same desk relaying their video through a TURN server on another continent, with no error anywhere and a bandwidth bill as the only symptom.

What to do

Instrument candidateType on the selected local candidate and alert when relay exceeds a few percent. For controlled deployments, IT can enable mDNS/Bonjour forwarding on the WLAN. For diagnosis, toggle chrome://flags/#enable-webrtc-hide-local-ips-with-mdns to see raw host candidates and confirm the LAN path works at all.

IPv6-only and NAT64 networks break TURN configured by IP literaldeployment

On an IPv6-only network with DNS64/NAT64 — T-Mobile US, several corporate networks, and Apple's mandated test configuration — an IPv4 literal in RTCIceServer.urls is unreachable because there is no IPv4 path and no hostname for DNS64 to synthesise a NAT64 address from. Gathering simply produces no relay candidates. If you use turns: with an IP literal you also fail certificate validation, since the certificate almost certainly has no IP SAN.

What to do

Always configure TURN by hostname, never by address, and make sure the hostname has an AAAA record or is reachable through DNS64. Test on an IPv6-only network (macOS can create one via Internet Sharing with NAT64 enabled) before shipping to mobile.

The Impossible Call · Module 3, Lesson 1