Introduction

Every time you make a video call, play an online game, or transfer a file peer-to-peer, your device faces a quiet paradox: your router hides you from the internet.

Network Address Translation (NAT) lets millions of home devices share a handful of public IP addresses. Your router picks an outgoing port, rewrites your packets, and keeps a table to route replies back to you. It works beautifully for browsing — you reach out to servers, and replies come back. But when someone on the internet wants to reach you first, there is no obvious path in. You are, in networking terms, behind a NAT.

The same is true of the person you want to talk to. Two devices, each hiding behind its own router, need to establish a direct channel — without either side knowing the other's true address in advance. That is the NAT traversal problem, and the solutions it spawned — STUN, TURN, and ICE — are the invisible plumbing behind every real-time peer-to-peer application on the web today.

Punch a Hole

The demo below simulates how two peers — each behind their own NAT — discover candidates and negotiate a connection path using the ICE algorithm.

<!-- {{c_layout_comment}} -->
<div class="scene">
  <div class="peer-box" id="peerA">
    <div class="peer-label">{{label_peer_a}}</div>
    <div class="peer-ip private">{{label_private}}: <span id="ipA-priv">192.168.1.42</span></div>
    <div class="peer-ip public" id="ipA-pub-row">{{label_public}}: <span id="ipA-pub">?</span></div>
    <div class="nat-tag">NAT</div>
  </div>
  <div class="middle-col">
    <div class="server-box stun-box">
      <div class="server-label">STUN</div>
      <div class="server-sub">{{label_stun_sub}}</div>
    </div>
    <div class="channel-box" id="channel-box">
      <div class="channel-label">{{label_signal}}</div>
      <div id="candidates-list" class="candidates-list"></div>
    </div>
    <div class="server-box turn-box" id="turn-box">
      <div class="server-label">TURN</div>
      <div class="server-sub">{{label_turn_sub}}</div>
    </div>
  </div>
  <div class="peer-box" id="peerB">
    <div class="peer-label">{{label_peer_b}}</div>
    <div class="peer-ip private">{{label_private}}: <span id="ipB-priv">10.0.0.7</span></div>
    <div class="peer-ip public" id="ipB-pub-row">{{label_public}}: <span id="ipB-pub">?</span></div>
    <div class="nat-tag">NAT</div>
  </div>
</div>
<div class="log-area" id="log"></div>
<div class="status-bar" id="status-bar">{{status_idle}}</div>
<div class="btns">
  <button id="btn-run" type="button">{{btn_run}}</button>
  <button id="btn-reset" type="button" class="ghost">{{btn_reset}}</button>
</div>
/* {{c_base_styles}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; font-size: 14px; }
.scene { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 8px; }
.peer-box { flex: 1; border: 2px solid #1d3557; border-radius: 10px; padding: 10px 8px; background: #edf2f7; min-width: 0; }
.peer-label { font-weight: 700; font-size: 15px; color: #1d3557; margin-bottom: 6px; }
.peer-ip { font-size: 12px; color: #444; line-height: 1.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.peer-ip.public { color: #0a7d33; font-weight: 600; }
.nat-tag { display: inline-block; margin-top: 6px; background: #1d3557; color: #fff; border-radius: 4px; padding: 2px 7px; font-size: 11px; font-weight: 700; letter-spacing: .05em; }
/* {{c_middle_styles}} */
.middle-col { display: flex; flex-direction: column; gap: 6px; width: 130px; flex-shrink: 0; }
.server-box { border-radius: 8px; padding: 7px 8px; text-align: center; }
.stun-box { background: #dbeafe; border: 1px solid #93c5fd; }
.turn-box { background: #fef3c7; border: 1px solid #fcd34d; opacity: .5; transition: opacity .4s; }
.turn-box.active { opacity: 1; }
.server-label { font-weight: 700; font-size: 13px; }
.server-sub { font-size: 10px; color: #555; margin-top: 2px; line-height: 1.35; }
.channel-box { border: 1px dashed #6b7280; border-radius: 8px; padding: 6px 8px; background: #f9fafb; }
.channel-label { font-weight: 600; font-size: 11px; color: #555; margin-bottom: 4px; }
.candidates-list { font-size: 10px; color: #374151; line-height: 1.55; min-height: 20px; }
/* {{c_log_styles}} */
.log-area { height: 110px; overflow-y: auto; background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 8px; padding: 6px 8px; font-size: 11px; font-family: ui-monospace, monospace; color: #333; margin-bottom: 6px; }
.log-area .entry { border-bottom: 1px solid #e2e8f0; padding: 2px 0; line-height: 1.45; }
.log-area .entry.ok { color: #0a7d33; }
.log-area .entry.warn { color: #b45309; }
.log-area .entry.relay { color: #c2410c; }
.log-area .entry.err { color: #c92f3c; }
.status-bar { font-size: 13px; font-weight: 600; min-height: 1.6em; margin-bottom: 6px; }
.status-bar.ok { color: #0a7d33; }
.status-bar.warn { color: #b45309; }
.status-bar.relay { color: #c2410c; }
.btns { display: flex; gap: 8px; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
/* {{c_packet_styles}} */
.packet { position: fixed; width: 10px; height: 10px; border-radius: 50%; pointer-events: none; z-index: 999; transition: none; }
// Code not found

Notice what happened: each peer first asked a STUN server for its public address (the address its NAT presents to the world), then both exchanged those ICE candidates through a shared signaling channel, and finally tried each candidate pair until one succeeded. If the direct path failed (strict NAT), the connection fell back to a TURN relay — slower, but reliable. This negotiate-then-connect dance is exactly what WebRTC runs every time you open a video call.

How It Really Works

NAT traversal has three layers, each solving a different piece of the puzzle.

STUN — discovering your public face

A STUN server (RFC 5389) sits on the open internet. You send it a packet; it replies with the public IP and port your NAT assigned to that packet. Now you know your server-reflexive candidate: the address the outside world sees for you. The trick is that your NAT, having opened that port to contact STUN, will now forward incoming packets from the same remote address — a property called endpoint-dependent filtering that most consumer NATs satisfy.

ICE — finding the best path

Interactive Connectivity Establishment (RFC 8445) is the algorithm that orchestrates everything. Both peers collect candidate addresses: their local LAN address, their STUN-discovered public address, and (if configured) a TURN relay address. They exchange these candidates through a signaling channel (anything out-of-band: a WebSocket, a server, even a phone call). Then ICE runs connectivity checks on every pair — it sends small STUN binding requests from each candidate to the peer's candidates and waits for replies. The pair that succeeds with the lowest priority score wins.

Hole punching — why it works (and when it doesn't)

When peer A sends a packet to peer B's public endpoint, A's NAT opens an outbound mapping. Simultaneously peer B does the same toward A. If both NATs use cone NAT semantics, B's packet arrives just as A's mapping is fresh — the hole is punched. The window is typically a few hundred milliseconds, which is why ICE fires both checks almost simultaneously.

The technique breaks under symmetric NAT, where the NAT assigns a different outbound port for each distinct destination. A's STUN-discovered address is only valid for packets going to the STUN server — peer B sees a completely different port. Here TURN is the only fallback.

TURN — the relay of last resort

A TURN server (RFC 5766) acts as a full relay: both peers connect to it, and all media flows through it. Latency rises and bandwidth costs increase, but the connection is guaranteed. Modern ICE implementations try direct paths first and fall back to TURN only when every direct candidate fails — which is why most WebRTC calls succeed without a relay even on corporate networks.

The overall success rate for direct hole punching across the open internet sits around 85–90 % — the remaining cases rely on TURN. Applications like maximum flow routing or distributed systems use similar "find any working path" strategies, but few are as cleverly adaptive as ICE.

Where It Matters

NAT traversal is not a curiosity — it is the foundation of almost every real-time networked application:

  • Browser video calls (WebRTC): every RTCPeerConnection runs ICE under the hood. The entire STUN/TURN/ICE stack was standardized precisely to make browser-to-browser media possible without a media server.
  • Online gaming: consoles and PC game engines use proprietary variants of hole punching to establish low-latency peer-to-peer sessions for multiplayer matches.
  • Overlay VPNs: tools like Tailscale and ZeroTier use NAT traversal to create encrypted tunnels between arbitrary devices on the internet, effectively building a flat LAN on top of an uncooperative internet.
  • File sharing and torrents: BitTorrent's NAT traversal extension (BEP-55) and similar protocols let peers behind NATs seed and leech without port-forwarding.
  • IoT and remote access: devices deployed in the field — cameras, sensors, industrial controllers — often sit behind carrier-grade NATs with no fixed address; ICE-based protocols are the only scalable way to reach them.

The deeper pattern is: whenever two endpoints each think the other is "unreachable," an intermediary with a global view (STUN, a signaling server) can broker enough information for them to self-organize a direct channel. The same idea appears in distributed systems coordination, where nodes share partial state through a rendezvous point to reach global agreement.

Conclusion

NAT was designed to stretch the IPv4 address space — not to connect peers. Yet the internet is built on peer-to-peer communication, and the tension between those two facts spawned one of networking's most elegant hacks: use a third party to discover your public face, exchange candidates out-of-band, fire packets simultaneously to punch matching holes, and relay only when the direct path fails.

STUN, TURN, and ICE have been running quietly inside every video call and multiplayer game for years. The next time a call connects in seconds despite two layers of routers, that is ICE running its candidate dance — and finding a path where, on paper, there should be none.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/nat-traversal/Content licensed under CC BY-NC 4.0.