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

STUN: Asking a Stranger for Your Address

Classic STUN simply does not work sufficiently well to be a deployable solution.— RFC 5389, 2008

By the end of this lesson

  • Parse and build STUN binding messages from raw bytes
  • Decode XOR-MAPPED-ADDRESS including the port XOR against the magic cookie
  • Explain why classic STUN's NAT-classification approach failed and how ICE repurposed STUN as its connectivity probe
  • Explain why the address is XORed (payload-rewriting ALG NATs)

Historical note

In March 2003, Jonathan Rosenberg, Joel Weinberger, Christian Huitema, and Rohan Mahy published RFC 3489 — Simple Traversal of UDP Through NATs, the original STUN. Its motivating wound was SIP (RFC 3261, June 2002): signaling would traverse the open internet just fine, and then the media, addressed to a private 10.x address scribbled inside the SDP, would fall into the void behind the caller's NAT. RFC 3489 proposed a seductive two-step cure: probe a server, classify your NAT into one of four types — full cone, restricted cone, port-restricted cone, symmetric — then choose a traversal strategy accordingly. Real NATs refused to be classified. Their behavior varied per destination, per port, per firmware revision, per how full the mapping table happened to be that afternoon. RFC 4787 (Audet and Jennings, January 2007) demolished the taxonomy, and RFC 5389 (October 2008) rebuilt STUN with a humbler name — Session Traversal Utilities for NAT — and one perfect trick: telling you what your address looks like from the outside. That mirror, refined once more in RFC 8489 (February 2020), is what every WebRTC call still holds up first.

3.2.1The classification dream, and why it collapsed

Classic STUN's protocol machinery was built around an interrogation. The client sent Binding requests to a server that owned two public IP addresses, attaching a CHANGE-REQUEST attribute that asked the server to answer from its other address, or its other port, or both. By comparing which probes got answered and whether the mapped address changed between them, the client sorted its NAT into RFC 3489's four bins. A "full cone" NAT meant anyone could reach you at your mapped address; "symmetric" meant the mapping itself changed for every destination, so an address learned from the STUN server was worthless for reaching anyone else. Classify once at startup, the theory went, then act.

The theory failed on contact with hardware. Five years of deployment experience — much of it from the VoIP boom that Skype (2003, Niklas Zennström and Janus Friis) had ignited by proving proprietary hole-punching worked at consumer scale — surfaced three fatal problems. First, the four types were not real categories: shipping NATs mixed behaviors, changing their mapping discipline under load, after a firmware update, or between UDP flows. Second, the classification answered the wrong question. It described the path between you and the STUN server, but the path that matters is the one between you and your peer, which may cross different NATs — including carrier-grade ones stacked behind your own. Third, the answer had a shelf life: a NAT that behaved one way during the probe could behave differently thirty seconds into the call.

The standards response came in two documents. RFC 4787 (January 2007), from the IETF's BEHAVE working group, replaced the four types with orthogonal mapping and filtering behaviors and told NAT vendors which combination to build. RFC 5389 (October 2008) then demoted STUN from a solution to a toolbox — the acronym's new expansion, Session Traversal Utilities for NAT, is a confession. The classification algorithm was deleted outright; a diagnostic descendant survives only as the experimental RFC 5780 (MacDonald and Lowekamp, May 2010), useful for network troubleshooting and explicitly not for call setup. RFC 8489 (February 2020) is the current revision, adding SHA-256 message integrity and algorithm agility while leaving the wire format you are about to learn untouched. What replaced classification was a change of philosophy: stop predicting whether a path will work, and try every path empirically. That philosophy is ICE, the subject of § 3.4 — and it kept STUN as its probe.

Laptop 10.0.1.7:51000 NAT rewrites src STUN server 198.51.100.9:3478 Binding request (20 bytes, no attributes) Binding success response: XOR-MAPPED-ADDRESS = 203.0.113.7:54321 "Here is what your address looks like from out here."
Figure 3.2.1 — STUN as a mirror. The server does one thing: it copies the source address and port it observed on the request into the response. The laptop learns its server-reflexive address — the NAT's public face for this socket.

3.2.2The message on the wire

STUN is refreshingly small. Every message begins with a fixed 20-byte header, followed by zero or more attributes. The header packs four fields:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0|   STUN message type       |        message length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  magic cookie = 0x2112A442                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                 transaction ID (96 bits)                      |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Listing 3.2.1 — The STUN header (RFC 8489 §5). All fields are big-endian.

The two leading zero bits are a demultiplexing gift: they guarantee a STUN packet's first byte is in the range 0–3, which can never collide with RTP or RTCP (whose first byte starts with version bits 10, giving values ≥ 128) or with DTLS (20–63). This is what lets WebRTC run STUN, DTLS, and SRTP over the same UDP socket and sort arriving packets by their first byte (RFC 7983 catalogs the scheme).

The 14-bit message type encodes a method and a class woven together: the class lives in two bits at positions 0x0100 and 0x0010. For the Binding method — essentially the only method plain STUN has — the four combinations are 0x0001 (request), 0x0101 (success response), 0x0111 (error response), and 0x0011 (indication, which expects no reply). The 16-bit message length counts the attribute bytes only, excluding the 20-byte header — a bare Binding request has length zero, and since attributes are padded, a valid length is always a multiple of four. Getting this field wrong is the classic first bug in every hand-rolled STUN encoder, which is why the exercise below traps it.

The magic cookie, the constant 0x2112A442, did not exist in RFC 3489, which used a full 128-bit transaction ID. RFC 5389 carved the top 32 bits off that ID and froze them, killing two birds: a server can now tell a modern client from a 2003-vintage one, and any packet inspector can cheaply recognize STUN by checking bytes 4–7. The remaining 96-bit transaction ID is chosen randomly per transaction; the server echoes it verbatim, letting a client with several requests in flight match each response to its question.

After the header come attributes in plain type–length–value form: a 16-bit type, a 16-bit length of the value in bytes, then the value padded up to a 32-bit boundary (the padding is not counted in the attribute's length field — the second classic bug). Attribute types below 0x8000 are comprehension-required: an implementation meeting an unknown one must fail the transaction. Types from 0x8000 up are comprehension-optional and can be skipped — SOFTWARE (0x8022), a free-text vendor string, is the one you will see everywhere in packet captures.

SIP ALGs on consumer routers still mangle STUN and SDPdeployment

XOR-MAPPED-ADDRESS exists because early NATs rewrote anything in a payload that resembled an address. Application-layer gateways on consumer routers — often on by default, often labelled "SIP ALG" — still rewrite SDP bodies and occasionally still mangle STUN. The signature is a srflx candidate with a plausible address but the wrong port, so connectivity checks are sent to a mapping that does not exist and every pair fails while STUN itself appears to have succeeded.

What to do

When a specific user's pairs all fail with correct-looking candidates, have them disable SIP ALG on their router. Compare the srflx port against the local port: a NAT that preserves ports on some flows and not others, or reports a port nothing is listening on, is the tell.

3.2.3XOR-MAPPED-ADDRESS, and the pettiest fix in the RFC series

The payload of the mirror is a single attribute. RFC 3489 used MAPPED-ADDRESS (type 0x0001): one reserved byte, one address-family byte (0x01 for IPv4, 0x02 for IPv6), a 16-bit port, and the raw address. Then reality intervened again. Many consumer NATs shipped with application-layer gateways — payload-rewriting helpers designed for protocols like FTP and SIP that carry literal IP addresses inside their message bodies. An ALG scans outgoing and incoming packets for anything that looks like the private address or the mapping it just created, and helpfully rewrites it. Applied to STUN, this "help" was sabotage: the NAT would find the mapped address the server had reported inside the response payload and translate it back, silently destroying the one fact STUN exists to deliver. The client could not even detect the tampering.

The fix, standardized in RFC 5389 as XOR-MAPPED-ADDRESS (type 0x0020), is gloriously unsophisticated: obfuscate the address so the ALG's pattern-matcher cannot recognize it. No cryptography, no key — just XOR against constants the ALG does not know to apply:

  • X-Port is the mapped port XORed with the most significant 16 bits of the magic cookie: port ^ 0x2112.
  • X-Address (IPv4) is the four address bytes XORed with the four cookie bytes 21 12 A4 42.
  • For IPv6, the 128-bit address is XORed with the concatenation of the magic cookie and the 96-bit transaction ID.

Decoding is the same operation — XOR is its own inverse. The rite of passage is forgetting that the port is XORed too: your decoder emits the right IP and a port like 62755 instead of 54321, and nothing connects. Here is a worked decode of the exact response used in Problem 1:

01 01 00 0c   type 0x0101 (Binding success), length 12
21 12 a4 42   magic cookie
b7 e7 a7 01   transaction ID (12 bytes,
bc 34 d6 86     echoed from the request)
fa 87 df ae
00 20 00 08   attr XOR-MAPPED-ADDRESS, value length 8
00 01 f5 23   family IPv4; X-Port 0xf523 ^ 0x2112 = 0xd431 = 54321
ea 12 d5 45   X-Address ^ 21 12 a4 42 = cb 00 71 07 = 203.0.113.7

Listing 3.2.2 — A complete 32-byte Binding success response, annotated. The reflexive transport address is 203.0.113.7:54321.

Note what the XOR is not: it is not secrecy. The cookie is a public constant; anyone on the path can decode the address trivially. It is armor against exactly one adversary — a well-meaning middlebox with a regex — which makes it perhaps the most precisely targeted kludge in the protocol suite. RFC 5389 kept MAPPED-ADDRESS alive alongside it only for backward compatibility with RFC 3489 clients; modern servers put the truth in the XORed attribute.

Check your understanding

Auto-graded check

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

Interactive 3D instrument

Asking a stranger for your address — the STUN binding transaction

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.

3.2.4STUN's second life: probe, hole punch, heartbeat

Had STUN remained only a mirror, it would be a footnote. Its real importance is that ICE — RFC 5245 (Jonathan Rosenberg, April 2010), revised as RFC 8445 (Keränen, Holmberg, and Rosenberg, July 2018) — adopted the STUN Binding transaction as its universal instrument, in three distinct roles.

Gathering. First the familiar role: during candidate gathering, the browser sends Binding requests from each local socket to the configured STUN server and harvests the XOR-MAPPED-ADDRESS as a server-reflexive (srflx) candidate. In an RTCPeerConnection this is what the { urls: 'stun:…' } entries in iceServers are for. Crucially — this is the lesson RFC 3489 taught — the browser makes no judgment about whether that address will work. It is one guess among several, thrown into the pile with host and relay candidates.

Connectivity checks. Then the repurposing. Once both sides have exchanged candidates over signaling, ICE pairs them up and sends STUN Binding requests directly between the peers — from each local candidate to each remote one. No STUN server is involved; each peer temporarily is a STUN server for its counterpart. A check that comes back proves, empirically, that this particular path forwards packets in both directions — and the request itself punches the hole, creating the NAT mapping it then tests. These peer-to-peer checks carry attributes plain mirror-STUN never needed: PRIORITY (0x0024), USE-CANDIDATE (0x0025) for nominating the winning pair, ICE-CONTROLLING/ICE-CONTROLLED (0x802A/0x8029) for role conflicts, and a MESSAGE-INTEGRITY HMAC keyed with the ice-ufrag/ice-pwd credentials the peers exchanged in their SDP — so a stray or malicious packet cannot pass as a check. The response's XOR-MAPPED-ADDRESS still earns its keep: if it reveals an address the peer never signaled, ICE has discovered a peer-reflexive candidate and adds it to the pile mid-flight.

Keepalives and consent. Finally, STUN outlives connection setup. NAT bindings are leases, not grants: an idle UDP mapping is typically evicted within tens of seconds. RFC 8445 §11 keeps the selected pair warm with STUN Binding indications every 15 seconds by default. WebRTC layers on a stricter discipline, consent freshness (RFC 7675, October 2015): full Binding request–response transactions roughly every 5 seconds, and if 30 seconds pass without a verified response the browser stops sending media entirely — a liveness check and an anti-abuse measure in one. When a call drops the moment a laptop switches Wi-Fi networks, this is the machinery that noticed. And when the direct path cannot be found at all, the same Binding transaction is used to talk to a relay — but allocating one is TURN's business, and the next lesson's.

A common error

"STUN servers relay your media, so a public STUN server can see your call." They do not and cannot. A STUN server answers 20-byte questions with ~32-byte answers and then forgets you; media never touches it, which is why one free server can serve millions of peers. The component that does relay media when direct paths fail is TURN — a separate protocol (an extension of STUN's message format, confusingly) with real bandwidth costs and real credentials. If you remember one contrast: STUN is a mirror, TURN is a pipe.

Problems for § 3.2Check your understanding

One byte-level parser built against canned wire data — no network needed, the traps are the real ones — and a short quiz on what STUN is and is not.

Lab 1

STUN bytes, no network

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

Check your understanding

Auto-graded check

4 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m3-l2-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.

Browsers do not gather reflexive candidates over TCPbrowser

stun:host:3478?transport=tcp is accepted in the configuration and then effectively ignored: browsers gather srflx over UDP only. If UDP to your STUN server is blocked, you get zero srflx candidates and no diagnostic — gathering just completes with fewer candidates than you expected, and ICE proceeds straight to relay. Any username and credential you supply alongside a stun: URL are also silently discarded; STUN in WebRTC gathering is unauthenticated.

What to do

Count candidate types during gathering. Zero srflx with non-zero host is a strong signal that UDP is blocked and that this session will relay. Do not put credentials on stun: entries; if you need authenticated fallback, that is what the TURN entry is for.

More STUN servers makes ICE slower, not more reliableperf

You get one srflx candidate per (local interface, STUN server) pair. A laptop with Wi-Fi, Ethernet, and a VPN adapter plus five STUN servers gathers fifteen srflx candidates that mostly duplicate each other, and the candidate-pair matrix grows as the product of both sides' candidate counts. More pairs means more connectivity checks competing for the same pacing budget, so time-to-connect gets worse. Chrome also caps the number of pairs it will check and will prune the ones you cared about.

What to do

One STUN server, or one plus a backup. Use iceCandidatePoolSize: 1 to warm gathering before you have a peer if setup latency matters. If you are chasing connect time, count pairs in getStats() — a healthy two-party call has single digits, not fifty.

ICE-STUN is not plain STUN, and unknown-attribute handling mattersspec

The binding requests browsers send during connectivity checks are RFC 8445 checks: they carry USERNAME, MESSAGE-INTEGRITY, FINGERPRINT, PRIORITY, and ICE-CONTROLLING/ICE-CONTROLLED, and use short-term credentials derived from the SDP ufrag and pwd. A homegrown STUN server that answers gathering requests correctly can still break connectivity if it mishandles comprehension-required attributes in the 0x0000–0x7FFF range, which must produce a 420 with UNKNOWN-ATTRIBUTES rather than being ignored.

What to do

Do not write your own. Use coturn or a maintained implementation. If you must debug one, Wireshark decodes STUN natively and will show you the attribute list and whether MESSAGE-INTEGRITY validated.

The Impossible Call · Module 3, Lesson 2