Day 03: UDP, TCP/IP & NAT Traversal
How two machines behind home routers manage to talk directly — the hardest, most-tested topic on the list.
⏱ ~2 hrs · theory 60 / lab 60
This is the chapter most likely to expose gaps in an interview, because it's where networking theory meets ugly
real-world reality. The internet was designed for every machine to have a public address; instead, billions of
devices hide behind NAT (Network Address Translation). P2P only works if peers can punch through
that. By the end of today you will have two UDP "peers" establishing a direct connection through a simulated
rendezvous server — the exact dance hyperdht performs.
3.1 TCP vs UDP — pick your tradeoff
| TCP | UDP | |
|---|---|---|
| Connection | Handshake, stateful | Connectionless, fire-and-forget |
| Delivery | Reliable, ordered, retransmitted | Best-effort; may drop/reorder/dup |
| Flow/congestion control | Built in | You build it (or don't) |
| Head-of-line blocking | Yes — one lost segment stalls the stream | No — packets are independent |
| Overhead | 20-byte header, ACK traffic | 8-byte header |
| NAT traversal | Hard (stateful, sequence numbers) | Easier (stateless datagrams) |
P2P systems lean heavily on UDP for two reasons. First, NAT hole-punching is far more reliable
with stateless datagrams than with TCP's handshake-and-sequence machinery. Second, head-of-line blocking: TCP
guarantees order, so a single lost packet stalls everything behind it — fatal for real-time or multiplexed
streams. This is exactly why QUIC (and HTTP/3) was built on UDP. The tradeoff is that you must implement whatever
reliability you actually need yourself, on top of UDP.
The mental model
TCP is a phone call: connection established, ordered conversation, you notice if the line drops. UDP is postcards:
cheap, independent, might arrive out of order or not at all — but you can drop one in any mailbox without setup.
3.2 What NAT actually does
Your router owns one public IP. Every device behind it has a private IP (192.168.x.x). When an inside
device sends a packet out, the router rewrites the source (private IP : port) to(public IP : some external port) and records the mapping in a translation table. Replies to that
external port get rewritten back and forwarded inside. The problem for P2P: an unsolicited inbound packet
from a stranger has no table entry, so the router drops it. Two peers behind NATs each expect the other to "call
first" — a deadlock.
The four NAT behaviors
| Type | Mapping rule | Punchable? |
|---|---|---|
| Full-cone | One external port per internal socket; anyone can use it | Easy |
| Restricted-cone | Same port, but only hosts you've sent to may reply | Yes |
| Port-restricted | Only the exact host:port you contacted may reply | Yes (most common case) |
| Symmetric | A new external port per destination — mapping is unpredictable | Hard / often needs relay |
The killer is symmetric NAT: because it allocates a different external port for every
destination, the port a peer learns via the rendezvous server is not the port that will be used to reach the
other peer. Hole-punching relies on predicting the external mapping, and symmetric NAT breaks that prediction. This
is the single most important NAT fact to be able to state crisply.
3.3 UDP hole punching, step by step
Both peers can make outbound connections; neither can receive unsolicited inbound. The trick is
to use a mutually reachable rendezvous (signaling) server to learn each other's public mappings,
then both fire packets at each other simultaneously. Each outbound packet opens a hole in its own NAT; when
the other peer's packet arrives, a matching table entry already exists, so it's allowed through.
- Peer A and Peer B each send a packet to the rendezvous server. The server observes their public
(IP:port)mappings (this is what STUN does). - The server tells A about B's public mapping, and B about A's.
- A and B both start sending UDP packets directly to each other's public mapping. The first packets may be dropped (no hole yet), but each one punches an outbound hole.
- Once both holes exist, packets flow directly — the server is no longer involved.
STUN, TURN, ICE — the standard stack
- STUN: a server that tells you your own public mapping ("what does the outside world see me as?"). Cheap, the basis of step 1.
- TURN: a relay that forwards traffic between peers when direct punching fails (e.g. symmetric NAT on both ends). Reliable but costs bandwidth — it's the fallback, not the goal.
- ICE: the algorithm that gathers all candidate addresses (local, STUN-derived, TURN-relayed), tries them in priority order, and picks the best working pair. WebRTC uses ICE.
Holepunch note
hyperdhtrolls its own version of this. The DHT itself is the rendezvous layer: peers announce
on a topic, the DHT coordinates the simultaneous-open, and a technique sometimes called "UDX" provides a
reliable-stream abstraction over UDP afterward. When you connect viahyperswarm, this whole sequence
happens under the hood. Being able to say "the DHT replaces the STUN/signaling server" is a strong interview answer.
3.4 Lab — simulate UDP hole punching locally
Goal
Build a rendezvous server and two peers using Node's dgram (UDP) module. The server introduces the
peers by their observed addresses; the peers then exchange packets directly. Running on localhost there's
no real NAT, but the protocol logic — observe address, exchange, simultaneous send — is identical to the
real thing.
Step 1 — the rendezvous server
// rendezvous.js — learns each peer's public address and introduces them
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
const peers = {};
server.on('message', (msg, rinfo) => {
const { name } = JSON.parse(msg);
// rinfo is the address the SERVER sees — i.e. the public mapping (STUN)
peers[name] = { address: rinfo.address, port: rinfo.port };
console.log(`registered ${name} @ ${rinfo.address}:${rinfo.port}`);
// once both peers are known, introduce them to each other
const names = Object.keys(peers);
if (names.length === 2) {
const [a, b] = names;
introduce(a, b); introduce(b, a);
}
});
function introduce(to, about) {
const payload = Buffer.from(JSON.stringify({ peer: about, addr: peers[about] }));
server.send(payload, peers[to].port, peers[to].address);
}
server.bind(9000, () => console.log('rendezvous on :9000'));
Step 2 — a peer
// peer.js — usage: node peer.js <myName>
const dgram = require('dgram');
const myName = process.argv[2] || 'peerA';
const sock = dgram.createSocket('udp4');
const RDV = { port: 9000, host: '127.0.0.1' };
let partner = null;
let connected = false;
sock.on('message', (msg, rinfo) => {
let data; try { data = JSON.parse(msg); } catch { data = { raw: msg.toString() }; }
// (a) introduction from the rendezvous server
if (data.addr) {
partner = data.addr;
console.log(`got partner ${data.peer} @ ${partner.address}:${partner.port}`);
startPunching();
return;
}
// (b) a direct packet from the partner — the hole is open!
if (data.hello) {
if (!connected) {
connected = true;
console.log(`✅ DIRECT connection established with ${data.hello}`);
}
return;
}
});
// punch: fire packets straight at the partner's public mapping repeatedly
function startPunching() {
const timer = setInterval(() => {
if (connected) return clearInterval(timer);
const pkt = Buffer.from(JSON.stringify({ hello: myName }));
sock.send(pkt, partner.port, partner.address); // opens our hole + may pass theirs
}, 200);
}
// register with the rendezvous server on startup
sock.send(Buffer.from(JSON.stringify({ name: myName })), RDV.port, RDV.host);
console.log(`${myName} registering…`);
Run it (three terminals)
# terminal 1
node rendezvous.js
# terminal 2
node peer.js peerA
# terminal 3
node peer.js peerB
# peerA output:
# peerA registering…
# got partner peerB @ 127.0.0.1:53412
# ✅ DIRECT connection established with peerB
What just happened (and what's different on real NAT)
- The server learned each peer's address as it observed it — that's the STUN principle. On real NAT this is the public mapping, not the private LAN address.
- Both peers fired at each other simultaneously; the repeated sends model the "first packets may be dropped while the hole opens" reality.
- Once connected, the server is never used again — true direct P2P.
- On symmetric NAT, the port the server observed would not match the port used peer-to-peer, so the punch fails and you'd fall back to a TURN-style relay. Try simulating this by having a peer send its punch packets from a different socket than the one it registered with — the introduction address no longer matches and the connection never forms.
3.5 Self-check & interview drill
- I can name the four NAT types and which one defeats hole punching.
- I can explain the rendezvous → observe → simultaneous-send sequence.
- I can define STUN, TURN, and ICE and how
hyperdhtreplaces the signaling server. - My two UDP peers established a direct connection through a rendezvous server.