From b890b78b94523371069b58e1a9c7e2a830348dde Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Sun, 19 Apr 2026 20:16:24 +0545 Subject: [PATCH 1/2] docs(site): add NAT-types reference page; link Homebrew on landing New standalone page at /nat-types/ that explains RFC 5780 mapping behaviours, why APDM (symmetric NAT) breaks direct P2P, how CGNAT is different, and when TURN is required. Written for developers debugging their own apps; natcheck appears as the diagnostic CTA near the bottom, not the subject. Also adds the Homebrew install path to the landing page now that the tap is live, and a "Background" link to the new reference. --- site/index.md | 11 +++++ site/nat-types.md | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 site/nat-types.md diff --git a/site/index.md b/site/index.md index 0586ae6..5e66687 100644 --- a/site/index.md +++ b/site/index.md @@ -21,6 +21,13 @@ Built on [`pion/stun`](https://github.com/pion/stun). ## Install +```bash +brew tap 1mb-dev/tap +brew install natcheck +``` + +or, on any platform with Go 1.25+: + ```bash go install github.com/1mb-dev/natcheck/cmd/natcheck@latest ``` @@ -35,6 +42,10 @@ go install github.com/1mb-dev/natcheck/cmd/natcheck@latest `$? -ne 2`: tool ran. `$? -eq 0`: direct P2P available. +## Background + +- [NAT types and why WebRTC connections fail]({{ '/nat-types/' | relative_url }}) — reference on RFC 5780 mapping behaviour, CGNAT, and when TURN is required. + ## Links - [Source & README](https://github.com/1mb-dev/natcheck) diff --git a/site/nat-types.md b/site/nat-types.md new file mode 100644 index 0000000..0341cfe --- /dev/null +++ b/site/nat-types.md @@ -0,0 +1,123 @@ +--- +layout: page +title: "NAT types and why WebRTC connections fail" +description: "A reference for WebRTC, P2P, and VPN developers: the four NAT mapping behaviours, why symmetric NAT breaks direct P2P, how CGNAT differs, and when TURN is required." +permalink: /nat-types/ +--- + +You shipped a WebRTC or P2P app. It works on your network and on most users' networks. But for some fraction of users, the direct peer-to-peer connection never completes, and either your app falls back to a relay server or the session just fails. This page is a reference for why that happens and how to tell which case you are in. + +It is written for developers debugging their own applications, not for network operators. It uses the RFC 5780 terminology and the older "cone / symmetric" labels side by side, because both still appear in documentation. + +## What a NAT actually does + +A NAT (Network Address Translator) is the device between your laptop and the public internet — usually a home router or a carrier-grade middlebox. When your laptop at `192.168.1.42:51820` sends a packet to a public server, the NAT rewrites the source address to one of its own public addresses, say `203.0.113.45:51820`, and remembers the mapping so it can rewrite replies back to your laptop. + +Direct peer-to-peer works by two NATed clients discovering each other's *public* address and port via a STUN server, then each side sending packets to the other's public endpoint. Whether those packets reach the other side depends entirely on two behaviours the NAT is free to choose for itself: **mapping** and **filtering**. + +## Mapping: what address does the NAT show to the world + +When your laptop sends packets from `192.168.1.42:51820` to two different servers, the NAT might use the *same* public port for both — or a different public port for each. This is the **mapping behaviour** and it has four variants in RFC 5780: + +| RFC 5780 term | Legacy name | Meaning | +|---|---|---| +| **Endpoint-Independent Mapping** (EIM) | Full Cone | Same public `IP:port` regardless of destination. | +| **Address-Dependent Mapping** (ADM) | Restricted Cone | Same public port per destination IP; new port per new IP. | +| **Address-and-Port-Dependent Mapping** (APDM) | Symmetric NAT | New public port per destination `IP:port` — effectively unpredictable from the outside. | +| No NAT | — | Laptop has a public IP directly. | + +The first three are all "NATed". The difference matters because peer-to-peer hole-punching requires the remote peer to predict your public port, and **APDM (symmetric NAT) makes that prediction impossible** — your NAT shows a different port to every remote peer, so the port discovered via STUN will not match the port used for the actual peer connection. + +## Filtering: who is allowed to reach you + +Once a mapping exists, the NAT decides which inbound packets it forwards to your laptop. Three variants: + +| RFC 5780 term | Meaning | +|---|---| +| **Endpoint-Independent Filtering** | Any external host can send packets to the mapping. | +| **Address-Dependent Filtering** | Only hosts with an IP you've sent to can reach you. | +| **Address-and-Port-Dependent Filtering** | Only the exact `IP:port` tuples you've sent to can reach you. | + +Filtering controls whether the *first packet* from your peer will be accepted — and it interacts with mapping to determine whether a "simultaneous send" hole-punch works. + +## The "will WebRTC work" matrix + +For direct peer-to-peer to succeed, both sides need a combination that allows hole-punching. The dominant factor is mapping: + +- **EIM × EIM:** direct P2P works reliably. Best case. +- **EIM × ADM or APDM:** usually works. The side with the stricter NAT receives first; the permissive side's predictable port lets the simultaneous-send trick punch through. +- **ADM × ADM:** often works with good ICE implementations. +- **APDM × anything:** **direct P2P fails**. The port presented via STUN does not match the port used when actually connecting to the peer, so hole-punching cannot succeed. You need a **TURN** relay. + +This is why "behind symmetric NAT" is a death sentence for direct WebRTC: the mapping behaviour is unpredictable, so no amount of clever ICE candidate gathering fixes it. + +## CGNAT is a separate problem + +Carrier-Grade NAT (CGNAT) is a second layer of NAT run by the ISP itself, typically on mobile carriers and some residential broadband (T-Mobile US, Jio India, Starlink, and others). Your router NATs you once; the carrier NATs you again. + +Detectable signal: your public IP falls in the `100.64.0.0/10` range reserved by RFC 6598 for shared CGNAT space. If you see a `100.64.x.x` address, you are behind CGNAT. + +Whether direct P2P works on CGNAT depends on the carrier's specific implementation, and the honest answer for most real networks is "it varies". Some carriers run permissive EIM/EIF CGNATs that hole-punch fine. Others run APDM CGNATs that don't. Without an active probe on the carrier in question, the only responsible answer is **unknown** — anything else is guessing. + +## So: STUN, TURN, or neither? + +- **STUN alone** is enough when both peers are behind EIM/ADM NATs with reasonable filtering. STUN servers are cheap — Google and Cloudflare run public ones for free. +- **TURN is required** when at least one peer is behind APDM (symmetric NAT) or a restrictive CGNAT. TURN relays the whole conversation through a server, so it costs bandwidth and money to operate. You cannot use public TURN servers in production; you run your own ([coturn](https://github.com/coturn/coturn) is the standard). +- **Neither** applies when a peer has a routable public IP (datacenter, IPv6 with no firewall, etc.) — increasingly common but not yet the default for residential users. + +The "will my users need TURN" question is really "what fraction of my users are behind APDM or hostile CGNAT" — a number that depends on your user base, not your code. + +## Diagnosing the network you're on + +To know which bucket a specific network falls into, you probe it with STUN from that network and compare responses across multiple servers. The short version: + +1. Send STUN Binding requests to two or more STUN servers on different IPs from the same local port. +2. If all servers report the **same** mapped public `IP:port`: EIM. Hole-punching friendly. +3. If servers report **different** mapped ports: ADM or APDM. The distinction requires RFC 5780's `CHANGE-REQUEST` attribute, which most public STUN servers do not support. +4. If your public IP is in `100.64.0.0/10`: you are behind CGNAT and the above probe is only telling you about the carrier's NAT, not your own. + +The tool that motivates this page, [natcheck](https://github.com/1mb-dev/natcheck), runs exactly this probe from your network and reports a WebRTC direct-P2P forecast. If you already know RFC 5780 cold, you do not need it — any STUN client plus some arithmetic gets you there. If you want a one-command answer and CI-friendly exit codes: + +```bash +brew tap 1mb-dev/tap +brew install natcheck +natcheck +``` + +or + +```bash +go install github.com/1mb-dev/natcheck/cmd/natcheck@latest +natcheck +``` + +Sample output: + +``` +Direct P2P: likely +NAT type: Endpoint-Independent Mapping (cone) +Public endpoint: 203.0.113.45:51820 + +Probes: + stun.l.google.com:19302 rtt=24ms mapped=203.0.113.45:51820 + stun.cloudflare.com:3478 rtt=31ms mapped=203.0.113.45:51820 + +Filtering not tested (v0.1). +``` + +natcheck reports `unknown` on CGNAT instead of guessing, because guessing is how wrong production assumptions get made. + +## What this page is not + +It is a developer-oriented reference, not a complete treatment of NAT traversal. The parts deliberately skipped: + +- **Hairpinning** (NAT forwarding packets back to another host behind the same NAT). Relevant if two clients on the same LAN connect to each other by public address. +- **UPnP / NAT-PMP / PCP** port mapping protocols. These let an application *request* a mapping rather than rely on STUN discovery. +- **ICE** — the algorithm WebRTC uses to try multiple candidate paths (host, server-reflexive via STUN, relayed via TURN) and pick the first that works. +- **QUIC and WebTransport** — newer transports with their own NAT traversal characteristics. + +Further reading: + +- [RFC 5780 — NAT Behavior Discovery Using STUN](https://www.rfc-editor.org/rfc/rfc5780) +- [RFC 6598 — CGNAT / 100.64.0.0/10](https://www.rfc-editor.org/rfc/rfc6598) +- [pion/stun](https://github.com/pion/stun) — the Go STUN implementation underneath natcheck From 5d17b42385411acd81f9db601da3162ad4a555ca Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Sun, 19 Apr 2026 20:26:11 +0545 Subject: [PATCH 2/2] docs(site): handcraft pass on NAT-types reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite to minimum-sufficient dev register: - Drop scene-setting opener and methodology preamble - Remove meta-self-references ("this page is a reference", "the tool that motivates this page") - Fix EIM × APDM row (was "usually works", really marginal — works only when APDM initiates) - Clarify ICE behaviour: ICE cannot hole-punch APDM; it falls back to TURN (not "ICE does not fix it") - Cut dramatic phrasings and moralizing closers - Tighten matching sections on index.md Same facts, ~30% shorter. Reads dev-to-dev. --- site/index.md | 2 +- site/nat-types.md | 108 ++++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 63 deletions(-) diff --git a/site/index.md b/site/index.md index 5e66687..2d6ff57 100644 --- a/site/index.md +++ b/site/index.md @@ -44,7 +44,7 @@ go install github.com/1mb-dev/natcheck/cmd/natcheck@latest ## Background -- [NAT types and why WebRTC connections fail]({{ '/nat-types/' | relative_url }}) — reference on RFC 5780 mapping behaviour, CGNAT, and when TURN is required. +- [NAT types and why WebRTC connections fail]({{ '/nat-types/' | relative_url }}) — RFC 5780 mapping, CGNAT, and when to use TURN. ## Links diff --git a/site/nat-types.md b/site/nat-types.md index 0341cfe..6ca170d 100644 --- a/site/nat-types.md +++ b/site/nat-types.md @@ -1,82 +1,70 @@ --- layout: page title: "NAT types and why WebRTC connections fail" -description: "A reference for WebRTC, P2P, and VPN developers: the four NAT mapping behaviours, why symmetric NAT breaks direct P2P, how CGNAT differs, and when TURN is required." +description: "Why some WebRTC and P2P sessions can't connect directly. The three RFC 5780 mapping behaviours, CGNAT, and when you need TURN." permalink: /nat-types/ --- -You shipped a WebRTC or P2P app. It works on your network and on most users' networks. But for some fraction of users, the direct peer-to-peer connection never completes, and either your app falls back to a relay server or the session just fails. This page is a reference for why that happens and how to tell which case you are in. +Some fraction of your users can't establish a direct WebRTC or P2P session. They fall back to a relay, or the session fails. -It is written for developers debugging their own applications, not for network operators. It uses the RFC 5780 terminology and the older "cone / symmetric" labels side by side, because both still appear in documentation. +## Mapping: what your NAT shows to the world -## What a NAT actually does +Your NAT sits between your laptop and the internet — usually a home router, sometimes an ISP middlebox. When your laptop at `192.168.1.42:51820` sends a packet to a public server, the NAT rewrites the source to one of its public addresses (say `203.0.113.45:51820`) and remembers the mapping for replies. -A NAT (Network Address Translator) is the device between your laptop and the public internet — usually a home router or a carrier-grade middlebox. When your laptop at `192.168.1.42:51820` sends a packet to a public server, the NAT rewrites the source address to one of its own public addresses, say `203.0.113.45:51820`, and remembers the mapping so it can rewrite replies back to your laptop. +When the same laptop sends to two different servers from the same local port, the NAT can use the same public port for both or a different port for each. That choice — the **mapping behaviour** — decides whether direct P2P works. -Direct peer-to-peer works by two NATed clients discovering each other's *public* address and port via a STUN server, then each side sending packets to the other's public endpoint. Whether those packets reach the other side depends entirely on two behaviours the NAT is free to choose for itself: **mapping** and **filtering**. - -## Mapping: what address does the NAT show to the world - -When your laptop sends packets from `192.168.1.42:51820` to two different servers, the NAT might use the *same* public port for both — or a different public port for each. This is the **mapping behaviour** and it has four variants in RFC 5780: - -| RFC 5780 term | Legacy name | Meaning | +| RFC 5780 | Legacy | Behaviour | |---|---|---| -| **Endpoint-Independent Mapping** (EIM) | Full Cone | Same public `IP:port` regardless of destination. | -| **Address-Dependent Mapping** (ADM) | Restricted Cone | Same public port per destination IP; new port per new IP. | -| **Address-and-Port-Dependent Mapping** (APDM) | Symmetric NAT | New public port per destination `IP:port` — effectively unpredictable from the outside. | -| No NAT | — | Laptop has a public IP directly. | +| **Endpoint-Independent Mapping** (EIM) | Full Cone | Same public `IP:port` for any destination. | +| **Address-Dependent Mapping** (ADM) | Restricted Cone | Same port per destination IP; new port per new IP. | +| **Address-and-Port-Dependent Mapping** (APDM) | Symmetric | New public port per destination `IP:port`. | -The first three are all "NATed". The difference matters because peer-to-peer hole-punching requires the remote peer to predict your public port, and **APDM (symmetric NAT) makes that prediction impossible** — your NAT shows a different port to every remote peer, so the port discovered via STUN will not match the port used for the actual peer connection. +Hole-punching needs the remote peer to predict your public port. APDM defeats that: the port a STUN server reports is not the port your NAT uses when you later contact your peer. ICE cannot hole-punch through APDM — it falls back to relaying via TURN. -## Filtering: who is allowed to reach you +## Filtering: who can reach an existing mapping -Once a mapping exists, the NAT decides which inbound packets it forwards to your laptop. Three variants: +Once a mapping exists, the NAT chooses which inbound packets to forward. -| RFC 5780 term | Meaning | +| RFC 5780 | Filtering | |---|---| -| **Endpoint-Independent Filtering** | Any external host can send packets to the mapping. | -| **Address-Dependent Filtering** | Only hosts with an IP you've sent to can reach you. | -| **Address-and-Port-Dependent Filtering** | Only the exact `IP:port` tuples you've sent to can reach you. | - -Filtering controls whether the *first packet* from your peer will be accepted — and it interacts with mapping to determine whether a "simultaneous send" hole-punch works. +| **Endpoint-Independent** | Any external host can send to the mapping. | +| **Address-Dependent** | Only hosts you've already sent to can reach you. | +| **Address-and-Port-Dependent** | Only exact `IP:port` tuples you've sent to can reach you. | -## The "will WebRTC work" matrix +Filtering controls whether the first inbound packet from your peer is accepted, and interacts with mapping to decide whether a simultaneous-send hole-punch works. -For direct peer-to-peer to succeed, both sides need a combination that allows hole-punching. The dominant factor is mapping: +## Will direct P2P work? -- **EIM × EIM:** direct P2P works reliably. Best case. -- **EIM × ADM or APDM:** usually works. The side with the stricter NAT receives first; the permissive side's predictable port lets the simultaneous-send trick punch through. -- **ADM × ADM:** often works with good ICE implementations. -- **APDM × anything:** **direct P2P fails**. The port presented via STUN does not match the port used when actually connecting to the peer, so hole-punching cannot succeed. You need a **TURN** relay. +Mapping on both sides is the dominant factor: -This is why "behind symmetric NAT" is a death sentence for direct WebRTC: the mapping behaviour is unpredictable, so no amount of clever ICE candidate gathering fixes it. +- **EIM × EIM:** works reliably. +- **EIM × ADM:** usually works. The EIM side has a predictable endpoint; the ADM side initiates and the EIM learns its source. +- **EIM × APDM:** marginal. Works when the APDM side initiates and the EIM responds to the observed source port; ICE candidate gathering alone is not enough. +- **ADM × ADM:** often works with a reasonable ICE implementation. +- **APDM × ADM or APDM × APDM:** fails. TURN relay required. -## CGNAT is a separate problem +## CGNAT is a second NAT in front of yours -Carrier-Grade NAT (CGNAT) is a second layer of NAT run by the ISP itself, typically on mobile carriers and some residential broadband (T-Mobile US, Jio India, Starlink, and others). Your router NATs you once; the carrier NATs you again. +Carrier-Grade NAT is a NAT your ISP runs in front of your own NAT. Common on mobile carriers (T-Mobile US, Jio India) and Starlink. Detectable because your public IP falls in `100.64.0.0/10`, the RFC 6598 shared address space. -Detectable signal: your public IP falls in the `100.64.0.0/10` range reserved by RFC 6598 for shared CGNAT space. If you see a `100.64.x.x` address, you are behind CGNAT. +CGNAT behaviour varies by carrier. Some run permissive EIM CGNATs that hole-punch fine; others run APDM and don't. Without an active probe on the specific carrier, `unknown` is the correct answer. -Whether direct P2P works on CGNAT depends on the carrier's specific implementation, and the honest answer for most real networks is "it varies". Some carriers run permissive EIM/EIF CGNATs that hole-punch fine. Others run APDM CGNATs that don't. Without an active probe on the carrier in question, the only responsible answer is **unknown** — anything else is guessing. +## STUN, TURN, or neither -## So: STUN, TURN, or neither? +- **STUN alone** works when both peers are behind EIM or ADM with reasonable filtering. Google and Cloudflare run public STUN servers for free. +- **TURN** is required when at least one peer is APDM or on a hostile CGNAT. TURN relays the full session, so it costs bandwidth. Don't use public TURN in production — run your own ([coturn](https://github.com/coturn/coturn) is the standard). +- **Neither** when at least one peer has a routable public IP (datacenter, open IPv6). Increasingly common; not yet default for residential users. -- **STUN alone** is enough when both peers are behind EIM/ADM NATs with reasonable filtering. STUN servers are cheap — Google and Cloudflare run public ones for free. -- **TURN is required** when at least one peer is behind APDM (symmetric NAT) or a restrictive CGNAT. TURN relays the whole conversation through a server, so it costs bandwidth and money to operate. You cannot use public TURN servers in production; you run your own ([coturn](https://github.com/coturn/coturn) is the standard). -- **Neither** applies when a peer has a routable public IP (datacenter, IPv6 with no firewall, etc.) — increasingly common but not yet the default for residential users. +"What fraction of my users need TURN" is a question about your user base, not your code. -The "will my users need TURN" question is really "what fraction of my users are behind APDM or hostile CGNAT" — a number that depends on your user base, not your code. - -## Diagnosing the network you're on - -To know which bucket a specific network falls into, you probe it with STUN from that network and compare responses across multiple servers. The short version: +## Diagnosing a specific network 1. Send STUN Binding requests to two or more STUN servers on different IPs from the same local port. -2. If all servers report the **same** mapped public `IP:port`: EIM. Hole-punching friendly. -3. If servers report **different** mapped ports: ADM or APDM. The distinction requires RFC 5780's `CHANGE-REQUEST` attribute, which most public STUN servers do not support. -4. If your public IP is in `100.64.0.0/10`: you are behind CGNAT and the above probe is only telling you about the carrier's NAT, not your own. +2. Same mapped public `IP:port` across servers → EIM. Hole-punching friendly. +3. Different mapped ports → ADM or APDM. Distinguishing them requires RFC 5780's `CHANGE-REQUEST` attribute, which most public STUN servers don't support. +4. Public IP in `100.64.0.0/10` → CGNAT. The probe characterises the carrier's NAT, not yours. -The tool that motivates this page, [natcheck](https://github.com/1mb-dev/natcheck), runs exactly this probe from your network and reports a WebRTC direct-P2P forecast. If you already know RFC 5780 cold, you do not need it — any STUN client plus some arithmetic gets you there. If you want a one-command answer and CI-friendly exit codes: +[natcheck](https://github.com/1mb-dev/natcheck) runs this probe and reports a direct-P2P forecast with exit codes a CI job can act on. Any STUN client plus the steps above works if you prefer to do it by hand. ```bash brew tap 1mb-dev/tap @@ -84,15 +72,13 @@ brew install natcheck natcheck ``` -or +or, with a Go toolchain: ```bash go install github.com/1mb-dev/natcheck/cmd/natcheck@latest natcheck ``` -Sample output: - ``` Direct P2P: likely NAT type: Endpoint-Independent Mapping (cone) @@ -105,19 +91,17 @@ Probes: Filtering not tested (v0.1). ``` -natcheck reports `unknown` on CGNAT instead of guessing, because guessing is how wrong production assumptions get made. - -## What this page is not +On CGNAT, natcheck reports `unknown` rather than guessing. -It is a developer-oriented reference, not a complete treatment of NAT traversal. The parts deliberately skipped: +## Not covered -- **Hairpinning** (NAT forwarding packets back to another host behind the same NAT). Relevant if two clients on the same LAN connect to each other by public address. -- **UPnP / NAT-PMP / PCP** port mapping protocols. These let an application *request* a mapping rather than rely on STUN discovery. -- **ICE** — the algorithm WebRTC uses to try multiple candidate paths (host, server-reflexive via STUN, relayed via TURN) and pick the first that works. -- **QUIC and WebTransport** — newer transports with their own NAT traversal characteristics. +- **Hairpinning** — NAT forwarding packets back to another host behind itself. +- **UPnP / NAT-PMP / PCP** — the app requests a mapping instead of inferring one via STUN. +- **ICE** — the algorithm WebRTC runs to try multiple candidate paths. +- **QUIC / WebTransport** — newer transports with different NAT traversal characteristics. -Further reading: +## References - [RFC 5780 — NAT Behavior Discovery Using STUN](https://www.rfc-editor.org/rfc/rfc5780) - [RFC 6598 — CGNAT / 100.64.0.0/10](https://www.rfc-editor.org/rfc/rfc6598) -- [pion/stun](https://github.com/pion/stun) — the Go STUN implementation underneath natcheck +- [pion/stun](https://github.com/pion/stun)