Skip to content

The CGNAT Version

Alex Logvin edited this page May 28, 2026 · 2 revisions

The CGNAT Version

If your home internet is behind CGNAT, the standard "set up a port forward and point a DDNS hostname at it" remote-access playbook doesn't work. This page explains why, and how Tailscale routes around the problem without any ISP cooperation.


What is CGNAT?

Carrier-Grade NAT is your ISP doing network address translation on top of the NAT your home router already does. Instead of every subscriber getting a unique public IPv4 address, hundreds (sometimes thousands) of subscribers share a single public IP, multiplexed by port.

The naming convention helps: CGNAT addresses live in the 100.64.0.0/10 range, reserved by RFC 6598 specifically for this purpose. If your WAN IP looks like 100.64.x.x, 100.95.x.x, or anywhere in that block, you're behind CGNAT.

Quick way to check:

# What your router thinks its WAN IP is (visible in the router admin UI)
# vs.
# What the public internet actually sees
curl ifconfig.me

If the two don't match AND your router's WAN IP starts with 100. (and isn't 100.127.255.x style local-IPv6-stuff), you're behind CGNAT.

Topology:

flowchart TB
    Internet["Public Internet"]
    ISP["ISP CGNAT gateway<br/>(one public IPv4 shared across subscribers)"]
    Sub1["Subscriber 1<br/>Home router (NAT #1)"]
    Sub2["Subscriber 2<br/>Home router (NAT #1)"]
    SubYou["You<br/>Home router (NAT #1)"]
    YourLAN["Your LAN devices<br/>(homelab, laptop, etc.)"]

    Internet --- ISP
    ISP --- Sub1
    ISP --- Sub2
    ISP --- SubYou
    SubYou --- YourLAN

    classDef shared fill:#fef3c7,stroke:#d97706,color:#222
    class ISP shared
Loading

Your homelab sits behind two layers of NAT: your home router (normal home NAT), then your ISP's CGNAT gateway. From the public internet, there's no way to address you specifically. Your "public" IP belongs to the ISP, shared with everyone else on that gateway.


Why ISPs use it

The short version: IPv4 addresses ran out.

The internet has about 4.3 billion IPv4 addresses. There are ~8 billion humans, each with multiple connected devices. The math doesn't math. Regional internet registries exhausted their free pools around 2011-2015 depending on region. Acquiring additional IPv4 blocks today means buying them from companies that don't need them anymore, at $30-50 per address. For an ISP with millions of subscribers, that's nine-figure money.

CGNAT defers the problem. By sharing a single public IP across hundreds of subscribers, the ISP needs ~1 IPv4 address per ~500 customers instead of one per customer. They keep selling service without buying expensive blocks.

Where you'll most commonly see it:

ISP type Typical CGNAT status
Mobile / 5G Home Internet (T-Mobile, Verizon, AT&T) Almost always
Starlink Default for residential
Apartment / condo bulk ISP Often
Rural fiber / WISP Frequently
Traditional cable / fiber (Comcast, Spectrum, AT&T fiber) Sometimes; usually optional with static-IP add-on

If you're on T-Mobile Home Internet or Starlink, you're behind CGNAT by default. There's no toggle in the customer portal that turns it off (with rare regional exceptions).

The long-term fix is IPv6, which has enough addresses (340 undecillion) to give every grain of sand on Earth its own subnet. Most ISPs support IPv6 alongside CGNAT-on-IPv4. But until every service you want to reach is dual-stack — which most aren't — the IPv4 path matters, and CGNAT is what you get.


Why CGNAT breaks remote access

Three things that normally work stop working:

1. Port forwarding is useless

The classic "open port 22 on the router to SSH home" pattern relies on your public IP belonging to you. With CGNAT, the public IP belongs to the ISP. Even if you set a port forward on your home router, inbound traffic from the internet never reaches the home router — it reaches the ISP's CGNAT gateway, which has no idea what to do with a port-22 connection on the shared address.

2. DDNS doesn't help

Dynamic DNS services map a hostname to whatever IP your home router reports. Behind CGNAT, the IP your router reports is its CGNAT-side address (a 100.x.x.x private address) or the shared public IP. Pointing myhomeserver.example.com at either one doesn't help anyone reach you.

3. Standard self-hosted VPNs can't accept inbound

OpenVPN, WireGuard, Tailscale's predecessor patterns: all assume the VPN server can receive an inbound connection on a known port. Behind CGNAT, there's no inbound. The VPN server can't listen for clients because the public internet can't reach it.

The practical consequence: you can browse the internet from your homelab (outbound works fine), but you can't reach your homelab from anywhere on the internet.


How Tailscale routes around it

Tailscale's architectural trick is outbound-initiated mesh. Every device on the tailnet, regardless of where it lives, makes outbound connections to Tailscale's coordination server. CGNAT, hotel WiFi, mobile data, corporate firewall — none of them block outbound traffic on standard ports.

Once a device has registered with the coordination server, it knows about the other devices in the same tailnet and starts trying to talk to them.

flowchart TB
    Coord["Tailscale Coordination Server<br/>(login.tailscale.com)"]
    Laptop["Your laptop<br/>(anywhere)"]
    Phone["Your phone<br/>(cellular)"]
    Homelab["Your homelab<br/>(behind CGNAT)"]

    Laptop -- "outbound" --> Coord
    Phone -- "outbound" --> Coord
    Homelab -- "outbound" --> Coord

    Laptop -. "WireGuard mesh<br/>via NAT traversal" .- Homelab
    Phone -. "WireGuard mesh<br/>via NAT traversal" .- Homelab
    Laptop -. "WireGuard mesh<br/>via NAT traversal" .- Phone

    classDef cgnat fill:#fef3c7,stroke:#d97706,color:#222
    class Homelab cgnat
Loading

Two devices that need to talk to each other use NAT traversal. Three mechanisms in layers:

  • STUN (Session Traversal Utilities for NAT) — each peer asks a public STUN server "what IP and port do you see me from?" and gets back its externally-visible endpoint. The two peers exchange this discovery via the coordination server.
  • ICE (Interactive Connectivity Establishment) — rather than pre-characterizing every NAT type and picking a strategy, Tailscale probes all candidate endpoints simultaneously and picks the first that works. The Tailscale engineering team's own description of this is "try everything at once, and pick the best thing that works." Empirically more robust than trying to identify NAT type up front.
  • DERP (Designated Encrypted Relay for Packets) — when ICE fails entirely, encrypted WireGuard packets are shuttled through a regional Tailscale-operated relay. The relay can't see the contents (end-to-end encrypted) but it can forward them. Slower than a direct connection, but it always works.

The cases where direct connection is hardest:

  • Symmetric NAT — assigns a different external port for every destination, so the STUN-discovered port doesn't match the port your peer needs to send to. Birthday-paradox port-prediction sometimes works, but slowly. Symmetric NAT is common on cellular carriers and some CGNAT deployments.
  • Hairpinning broken — when a NAT can't route a packet back to itself via its external IP, certain traversal paths fail. CGNAT gear is hit-or-miss on hairpinning; some implementations get it right, many don't.

In both cases, DERP is the safety net. It's the reason Tailscale works in environments where every other VPN architecture would give up.

If you want the deep technical version, Tailscale's blog post How NAT traversal works is one of the best networking writeups on the public internet. Worth reading regardless of whether you ever deploy Tailscale.

End result: your laptop can SSH into your homelab, your phone can stream from your Plex server, your tablet can reach Sonarr — all without any inbound connection from the public internet. Your ISP doesn't have to do anything. Your router doesn't need port forwards. CGNAT, formerly the boss-fight of self-hosting, becomes irrelevant.


The reframe

The classic homelab playbook treats port forwarding as the goal and CGNAT as an obstacle. That's the wrong frame.

You don't need port forwarding. You need access to your stuff.

Port forwarding is just one path to that goal, and it carries a long list of dependencies:

  • A public IP that belongs to you (CGNAT breaks this)
  • An ISP that doesn't NAT you (CGNAT breaks this)
  • A router you can configure (rentals, ISP gateways, locked-down firmware all break this)
  • A DDNS service to track changing IPs (still doesn't help behind CGNAT)
  • A degree of trust that nothing in this stack changes (ISP swaps you onto CGNAT one quiet Tuesday, your stack stops working)

And the security trade-off: every open port is an externally-reachable surface, scanned constantly by bots, indexed on Shodan, eventually probed by every CVE that drops in the next decade.

Tailscale doesn't recreate that path. It sidesteps the goal of "have a routable public IP" entirely. Every device dials home, the mesh forms outbound, access happens regardless of edge topology. No public ports. No DDNS. No scan surface.

The community has a more direct version of this argument: justfuckingusetailscale.com. Same thesis, more flair.


What Tailscale doesn't fix

Worth being honest about the boundary:

  • Sharing with non-Tailscale users. If you want to give Plex access to a family member who doesn't have a Tailscale agent installed, they still come in via plex.tv's relay (which is Plex's own CGNAT-mitigation, separate from Tailscale's). Tailscale solves remote access for devices you control, not for anonymous public sharing.
  • Public-facing services. For a service that needs to be world-reachable on a custom domain with no client install (like a public blog or a SaaS landing page), Tailscale Funnel covers some cases, but you're often better served by Cloudflare Tunnel or an outbound-tunneling load balancer.
  • Slow uplinks. CGNAT doesn't affect throughput, but the ISPs that lean heavily on CGNAT (mobile, Starlink) often have asymmetric or congested uplinks. Tailscale doesn't change physics.

The "ideal" Tailscale customer is anyone who wants identity-bound access to services they control. The bad fit is "I want to host a public-facing service on my CGNAT'd connection." Different problem.


What this means for the Plex Stack

The architecture deployed in this repo works identically whether your home internet is on a static public IP, a dynamic public IP with DDNS, or hard CGNAT.

Tailscale doesn't care which one you're on. The app container makes outbound connections to the coordination server, your laptop's Tailscale agent does the same, and the WireGuard mesh between them is established without anyone needing to listen for inbound connections at the edge.

For T-Mobile Home Internet and Starlink users especially, this is the most important consequence of the whole setup. Before Tailscale, the canonical answer to "can I self-host on T-Mobile Home Internet?" was "no, not really, the CGNAT will eat you alive." After Tailscale, the answer is "yes, exactly the same way you'd do it on any other connection." Same docker-compose. Same ACL. Same Serve URLs. Same https://app.YOUR-TAILNET.ts.net/sonarr/.


Related

Clone this wiki locally