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

The Module 3 Examination

The two most compelling problems facing the IP Internet are IP address depletion and scaling in routing.— K. Egevang & P. Francis, RFC 1631, 1994

What this module established

NAT was sold as a stopgap. RFC 1631 (May 1994, Kjeld Egevang and Paul Francis) proposed address translation as a "short-term solution" to IPv4 depletion, and RFC 1918 (February 1996, Rekhter and colleagues) fenced off 10/8, 172.16/12, and 192.168/16 as private space. The stopgap saved addressing and quietly murdered the end-to-end principle: two hosts that both sit behind translators cannot name each other, so for a decade peer-to-peer calling was effectively illegal physics. Skype (August 2003, Niklas Zennström and Janus Friis) proved the physics could be broken — calls connected from behind hotel routers — but proprietarily, a closed island. The open standardization of that folklore took fifteen years: STUN as RFC 3489 (March 2003, Rosenberg, Weinberger, Huitema, Mahy) tried to classify NATs and then act, failed in the field, and was demoted by RFC 5389 (October 2008) to a humble mirror; TURN (RFC 5766, April 2010; revised as RFC 8656, February 2020) supplied the relay of last resort that somebody has to pay for; and ICE (RFC 5245, April 2010, Jonathan Rosenberg; rewritten as RFC 8445, July 2018, Keränen, Holmberg, Rosenberg) wrapped the whole affair in the most intricate distributed state machine in WebRTC — built just to open a socket. RFC 4787 (January 2007, Audet and Jennings) gave us the modern vocabulary of mapping versus filtering behaviors, and Trickle ICE (RFC 8838, January 2021, though browsers trickled from the beginning) removed the pre-dial stall. This examination tests whether you can operate that machinery, read its wreckage, and explain its economics.

Plan on 90 minutes. Part A is code, graded by the harness with hints disabled and partial credit per assertion; it ends with a live loopback on a real RTCPeerConnection pair that must reach connected through signaling you write yourself. Part B is forensic scenario work on canned artifacts — logs, candidate tables, stats dumps. Part C is concepts and history, and, as every exam from Module 2 onward does, it reaches back with two spaced-review questions from earlier modules. The pass mark is 80 percent; retakes draw variants of these problems, so understanding beats memorizing the answer key.

AThe protocol bench

Three dry-bench problems, then a wet one. First you will script the decisions a controlling ICE agent makes — scheduling ordinary checks, honoring triggered checks, concluding with regular nomination — against four canned network scenarios. The contract below is a deliberately simplified slice of RFC 8445: pairs carry an id, a numeric priority, a state, and a nominated flag, and the agent object holds the checklist and the triggered-check queue.

// Shapes the grader passes to your functions:
//
//   pair  = { id: 'host-host', priority: 100,
//             state: 'waiting', nominated: false }
//   state is one of:
//     'frozen' | 'waiting' | 'in-progress' | 'succeeded' | 'failed'
//
//   agent = { checklist: [pair, ...], triggeredQueue: [pair, ...] }
//
// Contracts:
//   pairPriority(G, D)
//     G = controlling agent's candidate priority, D = controlled's.
//     Return a BigInt per RFC 8445 section 6.1.2.3:
//       2^32 * MIN(G,D)  +  2 * MAX(G,D)  +  (G > D ? 1 : 0)
//
//   nextCheck(agent)
//     Dequeue and return the head of triggeredQueue if non-empty;
//     else the highest-priority pair in state 'waiting';
//     else the highest-priority pair in state 'frozen';
//     else null. Never remove anything from the checklist itself.
//
//   handleBindingRequest(agent, pair)
//     A binding request arrived on this pair (a triggered check):
//     if the pair is 'succeeded', change nothing; otherwise set its
//     state to 'waiting' and enqueue it on triggeredQueue exactly once.
//
//   concludeRegular(agent)
//     Regular nomination: pick the highest-priority 'succeeded' pair,
//     set its nominated flag to true, and return it; return null
//     if no pair has succeeded yet.

Listing E3.1 — the agent contract for Problem 1.

Problem 2 hardens the candidate parser you wrote in § 3.4 against a nastier corpus: the candidate line as it appears in SDP, but now with IPv6 addresses, mDNS .local hostnames, an a= prefix, an uppercase transport token (Firefox emits UDP; Chrome emits udp), and TCP candidates carrying tcptype active on the discard port 9 (RFC 6544, 2012). Problem 3 is a STUN decode variant: raw bytes of binding success responses, in which you must validate the magic cookie 0x2112A442, walk the attribute list — no fixed offsets — and undo the XOR in XOR-MAPPED-ADDRESS. Recall the petty rationale from § 3.2: NAT application-layer gateways rewrote anything in a UDP payload that looked like an IP address, so RFC 5389 hid the address behind an XOR with the cookie: the port with the cookie's top 16 bits, the IPv4 address with all 32.

Part A — Coding, dry benchHints disabled · partial credit per assertion

Script the controlling agent, harden the parser, decode the bytes.

Lab 1

Script the controlling agent

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

Lab 2

Harden the candidate parser

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

Lab 3

Decode XOR-MAPPED-ADDRESS

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

A′The live wire

Everything above is dead reckoning. Problem 4 is navigation: build two peer connections in the same page, wire trickled candidates across the gap yourself — every non-null icecandidate event on one side becomes an addIceCandidate() on the other — run offer/answer, and deliver a pair whose caller reaches iceConnectionState of connected or completed inside ten seconds. Remember § 3.5: the null candidate is the end-of-gathering sentinel, not an error, and must not be forwarded. On a loopback pair there is no NAT between the peers, so the selected pair will be host↔host and connection is fast; the ten-second budget exists to catch signaling wired backwards, not slow networks.

Part A — Coding, liveReal RTCPeerConnection · 10-second budget

Reach connected through trickle signaling you write yourself.

Lab 4

Connect a trickled loopback

Graded in the browser against 1 assertion; the editor and harness require JavaScript.

BScenario diagnostics

Three incidents and two judgment calls, all drawn from artifacts of the kind webrtc-internals and getStats put in front of you. Read each artifact before answering; every correct diagnosis is anchored to a specific line. The stats excerpts use the real field names of candidate-pair entries — requestsSent, responsesReceived, requestsReceived, responsesSent — and the counters are cumulative, as § 3.5's survival primer warned.

Artifact B-1 — a call stuck in checking, both peers' gathered candidates and the configuration.

peer A gathered:
  candidate:2880323124 1 udp 2122260223 192.168.1.7 51472 typ host
  (end of candidates)
peer B gathered:
  candidate:1052258767 1 udp 2122260223 10.0.0.23 60111 typ host
  (end of candidates)

iceConnectionState: new -> checking     (t + 0.2 s)
iceConnectionState: checking            (t + 15.0 s, unchanged)

configuration: { iceServers: [] }
signaling log: offer delivered, answer delivered, 2 candidates
               delivered in each direction

Artifact B-2 — the same application, two networks. It works on home Wi-Fi and fails on the corporate LAN.

-- home Wi-Fi (connects) --
  typ host   192.168.1.7
  typ srflx  203.0.113.7  raddr 192.168.1.7   (via stun:stun.example.org:3478)
  selected pair: srflx <-> srflx, state: succeeded

-- office LAN (fails) --
  typ host   10.40.2.19
  stun:stun.example.org:3478                -> timed out
  turn:turn.example.org:3478?transport=udp  -> allocation timed out
  turn:turn.example.org:3478?transport=tcp  -> connection refused
  iceConnectionState: checking -> failed

IT confirms: outbound TCP on 80/443 only, via proxy; all UDP dropped;
             all other outbound ports blocked

Artifact B-3 — candidate-pair stats from a call that connected, but 100 percent over the relay.

candidate-pair #1   local srflx 203.0.113.7:54321
                    remote srflx 198.51.100.9:62044
  state: failed        requestsSent: 47    responsesReceived: 0
                       requestsReceived: 0 responsesSent: 0

candidate-pair #2   local relay 192.0.2.15:49802
                    remote relay 198.51.100.44:52990
  state: succeeded     nominated: true     requestsSent: 9
                       responsesReceived: 9
                       requestsReceived: 8 responsesSent: 8
Peer A NAT A (symmetric) STUN server Peer B src :54321 src :47102 B's check to :54321 × one mapping per destination — the door B aims at was never opened toward B
Figure E3.1 — The signature in Artifact B-3. An address-and-port-dependent ("symmetric") mapping allocates a fresh source port per destination: the STUN server saw :54321, but packets toward Peer B leave from :47102. B's checks aimed at :54321 die at the NAT, so the pair logs requestsSent > 0, responsesReceived = 0, requestsReceived = 0.

The last two questions in this part are judgment calls: recovering from a mid-call network migration, and a security review of what a DTLS-SRTP call actually exposes to the relay in the middle.

Part B — ForensicsFive scenarios, canned artifacts

Diagnose from the artifact; every answer is anchored to a line above.

Check your understanding

Auto-graded check

5 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m3-exam-quiz-b)

CConcepts, history, and spaced review

Short answers on the ideas that make the machinery legible: the candidate type that is never gathered, the nomination mode that did not survive 2018, the seconds Trickle ICE bought back, and why a TURN-percentage dashboard is a line item in a budget meeting. Then the spaced review reaches back — to Module 1's argument for datagrams and RTP, and to Module 2's offer/answer algebra (RFC 3264, 2002).

Part C — ConceptsPooled bank

Four questions on this module's ideas.

Check your understanding

Auto-graded check

4 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m3-exam-quiz-c)

Part C — Spaced reviewModules 1–2

Two questions from earlier modules, as promised.

Check your understanding

Auto-graded check

2 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m3-exam-quiz-review)

Examination score

The scorecard totals this page’s graded work as you go. It requires JavaScript.

The Impossible Call · Module 3 Examination