CGNAT for self-hosters: How to know port forwarding is not your problem

CGNAT for self-hosters: How to know port forwarding is not your problem

Thomas Byern's Field Notes

More like this: Self-hosting playbooks: checklists, setups, troubleshooting

Sometimes your router is fine, your port forward is fine, and your service is fine. The real problem is that your ISP never gave you a publicly reachable address in the first place.

I wasted an embarrassing number of evenings on this exact scenario at the very beginning some years back. Fresh Nginx Proxy Manager install, DNS records pointing where they should, firewall rules triple-checked. Everything looked correct on paper. But hitting my domain from a phone on mobile data returned absolutely nothing. Not a rejection, not a timeout page, just… nothing. Turns out my ISP had quietly moved my connection behind carrier-grade NAT months earlier, and no amount of router configuration was ever going to fix that.

If you are getting into self-hosting in 2026, there is a decent chance you will hit this wall too. IPv4 address exhaustion is not a theoretical future problem anymore. ISPs across Europe, Asia-Pacific, and increasingly North America are conserving their shrinking IPv4 pools by sharing a single public address among dozens or even hundreds of subscribers. The technology that makes this possible is called CGNAT, and it is the single most common reason that new self-hosters cannot get inbound traffic working despite doing everything else right.

This article will help you figure out whether CGNAT is your actual problem, and if it is, which escape route makes the most sense for your setup.

Before we continue

If this story helps you improve your homelab: 🔔 Follow me on Medium and subscribe to get my stories straight to your inbox.

What CGNAT actually is (and why it breaks everything you just configured)

To understand CGNAT, you need to understand what regular NAT does first, because you are already using it.

Your home router performs Network Address Translation every time any device on your LAN talks to the internet. You might have thirty devices at home, each with a private IP like 192.168.1.x, but your ISP only assigned one public IP to your router’s WAN interface. NAT is the mechanism that multiplexes all those private addresses behind that single public one. Outbound traffic works seamlessly because your router keeps a table of which internal device initiated which connection, and it routes return traffic back to the right place.

Port forwarding works within this model because you control the NAT device. You tell your router: “Any inbound traffic arriving on port 443, send it to 192.168.1.50.” Since your router holds the public IP, it can actually receive that inbound traffic and route it internally. The whole system depends on one assumption: that your router’s WAN address is the public address that the rest of the internet sees.

CGNAT breaks that assumption.

With carrier-grade NAT, your ISP inserts an additional layer of NAT between your router and the public internet. Your router’s WAN interface gets assigned an address, sure, but it is not a public address. It is a private address (often in the 100.64.0.0/10 range, which IANA reserved specifically for this purpose in RFC 6598) on the ISP’s internal network. The ISP’s CGNAT device then performs a second translation, mapping that intermediate address to a shared public IP that might serve dozens of other customers simultaneously.

So the traffic path looks like this:

Internet → 
  ISP's shared public IP → 
    CGNAT device (ISP-controlled) → 
      ISP internal network → 
        Your router's WAN (private IP) → 
          Your router's NAT → 
            Your server (LAN IP)

The critical problem is obvious once you see it drawn out: you can configure port forwarding on your router all day long, but inbound traffic from the internet never reaches your router in the first place. It arrives at the ISP’s CGNAT device, which has no idea you want port 443 forwarded anywhere. You do not control that device. You probably cannot even log into it. Your router is simply not the public edge anymore.

This is why CGNAT is so frustrating for beginners. Every tutorial, every forum post, every “how to self-host” guide assumes you have a real public IP. The port-forwarding steps are technically correct. They just do not apply to your network topology.

Why ISPs are doing this (and why it is getting more common)

There is no conspiracy here, just math. The IPv4 address space has roughly 4.3 billion usable addresses. The world passed that number of internet-connected devices a long time ago. Regional Internet Registries have been running on fumes for years. RIPE NCC (Europe, Middle East, Central Asia) exhausted its final /8 block back in 2019. ARIN (North America) has been rationing from a recovered pool since 2015.

For an ISP adding new subscribers, acquiring fresh IPv4 blocks means either buying them on the transfer market (where prices have climbed past 50 USD per address in some regions) or making do with what they have. CGNAT lets an ISP serve many customers from a smaller pool of public IPs, which is a perfectly reasonable engineering decision from their perspective. Most subscribers never notice because most consumer internet usage is outbound: browsing, streaming, gaming. All of that works fine behind CGNAT.

Self-hosting is the exception. The moment you want the internet to initiate a connection to you, the entire model falls apart.

How to prove you are behind CGNAT

Before you spend time on workarounds, you need to actually confirm that CGNAT is your problem. Plenty of other things can block inbound connections (ISP-level port blocking, misconfigured firewalls, DNS propagation delays), and you do not want to set up a tunnel only to discover the real issue was a typo in your Nginx config.

Here is a straightforward detection process. Work through it top to bottom.

Step 1: Compare your router’s WAN IP to your visible public IP

This is the fastest check and catches most CGNAT situations immediately.

Log into your router’s admin interface and find the IP address assigned to the WAN interface. This is usually on the status or overview page. Write it down.

Now, from any device on your network, visit a service that shows your public IP as the internet sees it. Plain old curl ifconfig.me from a terminal works, or use a browser and go to whatismyip.com or similar.

If these two addresses match, your router holds the public IP. You are almost certainly not behind CGNAT. Your connectivity issue is somewhere else (firewall, port forward config, DNS, ISP port blocking).

If they do not match, you are very likely behind CGNAT. Your router has been assigned an intermediate private address, and the ISP’s NAT device holds the real public IP.

One thing to watch for: if your router’s WAN IP falls in the 100.64.0.0/10 range (that is, 100.64.0.0 through 100.127.255.255), that is the RFC 6598 shared address space, and it is a near-certain indicator of CGNAT. Some ISPs use other private ranges (10.x.x.x is common too), but the 100.64.x.x block was literally created for carrier-grade NAT deployments.

Step 2: Run a traceroute and count the hops

If step one was inconclusive (maybe you have a double-NAT situation with your own equipment rather than ISP-level CGNAT), a traceroute can help clarify.

From a machine on your LAN, run:

traceroute -n 8.8.8.8 # Linux/macOS
tracert -d 8.8.8.8 # Windows

The -n / -d flag skips DNS resolution of intermediate hops, which speeds things up and makes the output cleaner.

Look at the first few hops. The first hop is typically your home router. If the second hop is an address in a private range (10.x.x.x, 100.64.x.x, 172.16–31.x.x, 192.168.x.x) before you reach anything public, there is a NAT device between you and the internet that you do not control. That is CGNAT.

A normal (non-CGNAT) traceroute from a home connection typically shows your router as hop 1 and then immediately jumps to a public IP at hop 2. A CGNAT traceroute shows one or more private-range hops before the first public address appears.

Step 3: Try an inbound connection test from outside your network

If you want definitive proof rather than inference, test actual inbound reachability. Start a simple listener on your server:

# Listen on port 9999 with netcat
nc -l 9999

Make sure your router has a port forward for 9999 pointing to that machine. Then, from a device not on your home network (a phone on mobile data, a VPS, a friend’s computer), try to connect:

nc -vz YOUR_PUBLIC_IP 9999

If the connection succeeds, you have inbound reachability and CGNAT is not your issue. If it times out despite correct port forwarding and no local firewall blocking, CGNAT (or ISP port filtering) is almost certainly the cause.

Step 4: Just ask your ISP

This sounds obvious, but it is genuinely the most reliable method, and a lot of people skip it. Call or chat with your ISP’s support and ask: “Is my connection behind carrier-grade NAT?” or “Do I have a dedicated public IPv4 address?”

Many ISPs will tell you directly. Some will offer to move you to a plan with a public IP (sometimes for free, sometimes for a small monthly fee). We will get to that option shortly.

Quick-reference detection checklist

For easy reference, here is the condensed version of the detection process:

If two or more of these point to CGNAT, you can be confident that is your problem. Time to pick an exit strategy.

Dynamic DNS: necessary but not sufficient

Before we get into the escape routes, a quick note on dynamic DNS, because it comes up in almost every self-hosting guide and it is easy to confuse what it solves with what it does not.

Most residential ISP connections assign your public IP dynamically. It might stay the same for weeks, or it might change every time your router reboots. Dynamic DNS (DDNS) services solve this by automatically updating a DNS record whenever your IP changes. You point your domain at your DDNS hostname, your DDNS client detects the new IP and pushes an update, and your domain keeps resolving to the right place.

This is useful and you will probably want it regardless of your CGNAT situation. But DDNS solves a different problem than CGNAT. DDNS handles “my IP keeps changing.” CGNAT is “inbound traffic cannot reach me at all.” If you are behind CGNAT, setting up DDNS will faithfully update your DNS record to point to… the ISP’s shared CGNAT address, which still will not route traffic to your home. You need reachability first. DDNS comes after.

That said, some of the escape routes below (particularly tunnels and VPS relays) give you a static endpoint anyway, which makes DDNS unnecessary for the tunneled services. Something to keep in mind as you evaluate options.

Your four exits

Once you have confirmed CGNAT is the issue, you have four realistic paths forward. Each involves tradeoffs around cost, complexity, performance, and how much control you retain. Let me walk through them honestly.

Exit 1: Request a public IPv4 address from your ISP

What it is: You contact your ISP and ask them to assign a dedicated public IPv4 address to your connection, removing you from the CGNAT pool.

How it works in practice: Some ISPs offer this as a free option you just have to ask for. Others charge a small monthly premium (typically 3 to 10 USD equivalent). A few business-tier plans include it by default. In some cases the ISP will need to reprovision your connection, which might mean a brief outage.

Advantages: This is the cleanest solution. Once you have a real public IPv4, standard port forwarding works exactly as every tutorial describes. No tunnels, no extra infrastructure, no added latency. You are back to the “normal” self-hosting model.

Drawbacks: Not every ISP offers it. Some providers, especially mobile broadband (4G/5G home internet) and certain budget fiber operators, physically cannot give you a dedicated IPv4 because their entire architecture assumes CGNAT. Even when available, the IP is typically still dynamic, so you will want DDNS on top.

Best for: Anyone whose ISP actually offers the option. Always try this first. It is the lowest-complexity solution.

Exit 2: Use IPv6

What it is: If your ISP assigns you a public IPv6 prefix (and many do, even when they CGNAT your IPv4), you can make your services reachable over IPv6 without any NAT at all.

How it works in practice: IPv6 does not use NAT in the same way. Your ISP typically delegates an entire /64 or /56 prefix to your router, and every device on your network gets a globally routable IPv6 address. You configure your router’s firewall to allow inbound traffic on specific ports to specific internal addresses, and you are done. No port forwarding in the NAT sense, just firewall rules.

Advantages: No monthly cost. No extra infrastructure. End-to-end connectivity the way the internet was originally designed to work. Latency is identical to a native connection because it is a native connection.

Drawbacks: The big one is that not all clients can reach you over IPv6. As of early 2026, global IPv6 adoption hovers around 45% of internet users (Google’s measurements put it slightly higher). That means roughly half of potential visitors or users may not have IPv6 connectivity and simply will not be able to reach your services. If you are self-hosting something just for yourself and your devices all support IPv6, this is less of an issue. If you are running something public-facing, you are cutting off a significant chunk of the internet.

There is also the DNS complexity. You need AAAA records instead of (or in addition to) A records. If you want dual-stack access, you still need a solution for IPv4 visitors, which circles back to one of the other exits.

Best for: Personal services where you control both ends (accessing your own Nextcloud from your own phone, for example). Also works well as a complement to another solution: use IPv6 natively where possible, fall back to a tunnel for IPv4 clients.

More about it: IPv6-first homelab: what broke, what worked, and a verification checklist

Exit 3: Use a tunnel service

What it is: A third-party service gives you a public endpoint (IP or hostname), and your home server maintains a persistent outbound connection to that service. Inbound traffic hits the tunnel provider’s endpoint, travels through the tunnel, and arrives at your server. Because the tunnel is initiated outbound from your network, CGNAT does not block it.

The main options in 2026:

Cloudflare Tunnel (formerly Argo Tunnel) is probably the most popular choice right now. You install the cloudflared daemon on your server, authenticate it with your Cloudflare account, and it establishes an outbound connection to Cloudflare's edge. You manage which hostnames route to which local services through Cloudflare's dashboard or a config file. The free tier covers HTTP/HTTPS traffic, which handles most web-based self-hosting needs (Nextcloud, Jellyfin's web UI, Home Assistant, Gitea, etc.). The catch is that non-HTTP traffic (raw TCP, UDP) requires the paid tier or is not supported at all, and all your traffic flows through Cloudflare's network, which means they can technically inspect it. Also, your domain must use Cloudflare's nameservers.

TailscaleHeadscale (the self-hosted Tailscale control server), and NetBird take a different approach. They build a WireGuard-based mesh VPN between your devices. It does not give you a publicly reachable endpoint by default, but it does let all your mesh-connected devices reach your home server regardless of CGNAT. If you want public access, Tailscale Funnel exposes specific services to the internet through Tailscale’s infrastructure. Tailscale is excellent for the “I just want to access my stuff from my own devices” use case and less ideal for “I want the general public to reach my website”.

More about it: NetBird vs Tailscale for homelabs: Sharing, ACLs, and “family usability”

WireGuard or OpenVPN to a VPS is the DIY version. You rent a cheap VPS with a public IP, set up a VPN tunnel between the VPS and your home server, and configure the VPS to forward traffic through the tunnel. This gives you complete control and works for any protocol, but it is more setup and maintenance. We will cover this in more detail under Exit 4, since the VPS is really the core of that approach.

Advantages of tunnels generally: They work regardless of your ISP situation. No need to negotiate with your provider, no need to change plans. Setup ranges from trivially easy (Cloudflare Tunnel) to moderately involved (self-managed WireGuard).

Drawbacks: Added latency, because every request takes a detour through the tunnel provider’s network. For Cloudflare Tunnel, the latency is usually small (Cloudflare has edge servers everywhere), but it is non-zero. For a VPS tunnel, the latency depends on the VPS location relative to you and your users. You also gain a dependency on a third-party service. If Cloudflare has an outage, your self-hosted services go down too, which somewhat undermines the “self” part of self-hosting.

Best for: Web-based services where a few milliseconds of added latency do not matter. Cloudflare Tunnel is particularly good for people who want minimal infrastructure to manage.

Exit 4: Rent a small VPS as a public relay

What it is: You rent the cheapest VPS you can find (we are talking 3 to 5 USD per month for a tiny instance), give it a public IP, and use it as a relay point. Your home server connects outbound to the VPS over WireGuard or similar. The VPS forwards inbound traffic through the tunnel to your home server.

How it works in practice: The VPS does not need to be powerful. It is not running your services, just forwarding packets. A single-core, 512 MB RAM instance is more than enough. You set up WireGuard on both ends (the VPS and your home server), establish the tunnel, and then configure port forwarding or reverse proxying on the VPS to send traffic through the tunnel. Tools like frp (Fast Reverse Proxy), or even plain iptables/nftables rules, handle the forwarding.

A typical setup:

  1. Provision a small VPS (Hetzner, BuyVM, RackNerd, Oracle Cloud free tier, wherever).
  2. Install WireGuard on the VPS and your home server. Establish the tunnel.
  3. On the VPS, forward ports (iptables DNAT/MASQUERADE) or run a reverse proxy (Nginx, Caddy) that proxies to the home server’s WireGuard address.
  4. Point your domain’s DNS records at the VPS’s public IP.

Traffic flow: Internet → VPS public IP → WireGuard tunnel → Your home server.

Advantages: Full control over every layer. Works for any protocol (HTTP, TCP, UDP, whatever you need). No third-party service dependency beyond the VPS provider. You get a static public IP, so you do not even need DDNS. You can also use the VPS for other lightweight tasks (DNS, monitoring, a small static site).

Drawbacks: More setup and ongoing maintenance than a managed tunnel. You are responsible for keeping the VPS patched and the WireGuard tunnel healthy. There is a monthly cost, though it is small. Latency depends on the VPS location; pick one geographically close to your home for the best results.

Best for: People who want to self-host non-HTTP services (game servers, mail, SSH, custom TCP protocols), who want full control, or who are uncomfortable routing everything through Cloudflare. Also the best option if you are already comfortable with basic Linux server administration.

More about it:

Comparison at a glance

Picking the right exit for your situation

With four options on the table, the decision comes down to a few practical questions about your specific circumstances.

Start by calling your ISP. Seriously. If they can hand you a public IPv4 for free or a few dollars a month, take it and move on. This eliminates the problem entirely with zero ongoing complexity. Do not over-engineer a solution when a phone call might fix everything.

If your ISP cannot or will not give you a public IPv4, check whether you have a usable IPv6 prefix. If you do, and your primary use case is accessing your own services from your own devices (all of which support IPv6), that might be enough on its own. Set up firewall rules, configure AAAA records, and you are done.

If you need general public reachability over IPv4 and your services are web-based (HTTP/HTTPS), Cloudflare Tunnel is the path of least resistance. The free tier handles most self-hosting scenarios, setup takes maybe fifteen minutes, and Cloudflare’s network is fast enough that the added latency is negligible for web traffic.

If you need to expose non-HTTP services, or you want to avoid depending on Cloudflare, the VPS relay is your best bet. The cost is modest, you get total protocol flexibility, and the skills you build managing it are genuinely useful. If you are getting into self-hosting, learning to administer a small Linux VPS is time well spent regardless.

And there is nothing wrong with combining approaches. IPv6 for direct device-to-device access, Cloudflare Tunnel for your public-facing web services, and maybe a VPS relay for that one game server your friends connect to. Use whatever makes sense for each service.

Wrapping up

The core lesson here is that port forwarding is not a universal solution. It is a technique that works under a specific condition: your router holds a public IP address. CGNAT violates that condition, and no amount of reconfiguring your router will change an architectural constraint imposed by your ISP.

The good news is that once you know CGNAT is the problem, the solutions are well-understood and none of them are particularly expensive or difficult. The detection process takes five minutes. Calling your ISP takes ten. Setting up a Cloudflare Tunnel or a WireGuard relay takes an afternoon at most.

The real waste of time is not knowing what you are looking for. Now you do. Go check your WAN IP, compare it to what the internet sees, and work from there.

And if you like what I’m doing here, I’d be thrilled if you’d consider buying me a coffee.