Beating CGNAT: Reaching a Homelab With No Public IP
Tunnels and relays for the day your ISP quietly takes your public IPv4 address away

Contents
The first sign is usually a service that used to be reachable from outside the house and suddenly isn’t. You check the router, the port forward is still configured exactly as it was, the local service is up, and yet nothing gets through. Then you check what IP address your router thinks it has on the WAN side, compare it against what a “what’s my IP” site says from outside, and they don’t match. That mismatch is CGNAT — Carrier-Grade NAT — and once an ISP has put you behind it, port forwarding is not broken, it’s simply irrelevant, because your router was never handed a public address to forward a port on in the first place.
Why CGNAT exists and why it breaks everything
IPv4 addresses ran out years ago in any practical sense, and ISPs facing subscriber growth with a shrinking pool of addresses had a choice: pay increasingly absurd prices on the IPv4 transfer market, push IPv6 rollout (which many are doing, slowly), or share one public IPv4 address across hundreds or thousands of customers using another layer of NAT at the ISP’s edge. Most large ISPs, especially on mobile and newer fibre rollouts, chose the third option, at least as a stopgap.
The result is that your router’s WAN address is itself a private-ish address (commonly in the 100.64.0.0/10 range reserved for exactly this purpose) sitting behind the ISP’s own NAT, which sits in front of the actual public internet. Port forwarding on your own router configures translation for your NAT layer — it has zero effect on the ISP’s NAT layer upstream, which you don’t control and typically can’t even see the configuration of. Nothing you do on your own hardware can open an inbound port through it. This is a deliberate ISP design choice, and it is very rarely something a support call fixes — the answer is usually “pay for a business tier with a static IP” or nothing at all.
Some ISPs deploy a close cousin called DS-Lite instead of straight CGNAT, tunnelling IPv4 over an IPv6-only access network rather than sharing a pool of IPv4 addresses directly. The symptom looks identical from the customer’s side — no usable public IPv4, port forwarding does nothing — but the underlying mechanism is different enough that it’s worth knowing which one you’ve got before you start troubleshooting, since some of the workarounds below (a relay VPS, a managed tunnel) apply regardless, while others (arguing with support about which NAT tier you’re on) depend on the ISP even being willing to say. Either way, the practical answer for a homelab is the same: nothing changes without either a paid static-IP tier or a tunnel of some kind, so it’s rarely worth spending much time distinguishing DS-Lite from plain CGNAT beyond satisfying curiosity.
Options, roughly cheapest to most involved
A relay through infrastructure you don’t control is the fastest fix and the one most people reach for first, which is exactly what a mesh VPN like Tailscale offers out of the box — see the earlier piece on Tailscale as a zero-config mesh. Every device runs the client, joins the same private mesh, and connectivity works regardless of what NAT layers sit between them, because WireGuard’s NAT traversal (aided by Tailscale’s DERP relay fleet when a direct path isn’t possible) handles exactly this case. It requires the client installed on anything that needs to reach in, which is a real limitation if the goal is a public HTTPS endpoint that random visitors — other people’s browsers, a webhook sender, anything without a Tailscale client — need to reach.
A relay through infrastructure you do control is the more general version of the same idea: a small VPS with a genuine public IPv4 address, running WireGuard, acting as a rendezvous point between the homelab and the outside world. The VPS needs almost no resources — a single vCPU box is overkill for the job — since all it does is forward packets back and forth.
| |
| |
The PersistentKeepalive line matters more here than in most WireGuard configs — it’s what keeps the CGNAT’d side’s outbound connection state alive on the ISP’s NAT table, since the homelab gateway is always the one initiating, never the one being connected to directly. Without it, the ISP’s NAT entry times out after a period of inactivity and the tunnel silently stops accepting inbound traffic until the homelab side sends something outbound again.
With the tunnel up, the VPS forwards a public port to the homelab side over the tunnel:
| |
This is the manual, fully self-owned version of what a paid tunnel service does automatically, and it’s covered in more depth — including the WireGuard side specifically as a homelab-access pattern — in the WireGuard homelab tunnel piece.
A managed tunnel service — Cloudflare Tunnel being the most common — skips running the relay VPS yourself entirely. cloudflared runs on a box in the homelab and opens an outbound-only connection to Cloudflare’s edge, which then proxies public HTTPS traffic in over that connection. No inbound port needed anywhere, because the homelab side never accepts a connection — it only ever initiates one, which is precisely the shape of connection CGNAT never blocks. The trade-off, examined properly in the Cloudflare Tunnels piece, is that Cloudflare sits in the traffic path for anything routed this way, which is a different trust model than a WireGuard tunnel to a VPS you rent and administer yourself.
Headscale, if it’s already running as the mesh’s control plane per the self-hosted Tailscale piece, solves the device-to-device case the same way hosted Tailscale does — DERP relay fallback when direct connection isn’t possible — while keeping the coordination server under your own control rather than depending on a third party’s for basic reachability between your own machines.
Picking a relay VPS and keeping the tunnel alive
If the self-run relay is the route taken, the VPS choice matters less than people expect and the tunnel’s resilience matters more. Almost any budget provider’s smallest instance handles WireGuard’s encryption overhead for typical homelab traffic without breaking a sweat — the actual constraints worth checking before signing up are the bandwidth allowance (some budget tiers cap monthly transfer low enough to matter if the relay is also carrying backups or media traffic), and location, since every relayed packet now takes a detour through the VPS’s region and that adds real, measurable latency for anything interactive.
The tunnel itself should survive a VPS reboot and a homelab-side reboot without manual intervention, which means a systemd unit rather than a manually-run wg-quick invocation:
| |
| |
Worth also confirming the relay-side firewall allows the WireGuard UDP port from anywhere (51820/udp in the example above) — cloud provider security groups default to deny-inbound on most ports, and it’s an easy step to forget between provisioning the VPS and testing the tunnel, leading to a confusing “it works from the VPS but not from the homelab” debugging session that’s actually just a missing security-group rule.
Weighing trust against convenience
The four options above aren’t interchangeable — they trade off differently on who has to be trusted and how much you have to maintain. A hosted mesh like Tailscale is the least maintenance but means every packet between meshed devices can, in principle, be routed through infrastructure Tailscale operates (direct peer-to-peer when possible, their DERP relays when not); you’re trusting their operational security and their promise about what they log. A self-run relay VPS flips that trade entirely: you maintain the box, patch it, and pay for it every month, but nobody except the VPS provider itself sits in the path, and even they only see encrypted WireGuard traffic. A managed tunnel like Cloudflare’s sits somewhere in between — no VPS to patch, but Cloudflare terminates and re-originates the connection, meaning they see plaintext HTTP even though the transport to them is encrypted, which matters if the service behind the tunnel handles anything sensitive.
None of these is the objectively correct choice; it genuinely depends on what’s being exposed and how much operational overhead is tolerable. I run a self-managed relay VPS for anything I want a real public IPv4 endpoint for, and lean on Tailscale for device-to-device access where a client is always available to install — the two coexist without conflict, since they’re solving slightly different problems (public reachability versus private mesh access).
The IPv6 escape hatch
CGNAT is specifically an IPv4 problem, born of IPv4 address scarcity, and it’s worth checking whether the ISP handing out a CGNAT’d IPv4 address is nonetheless handing out a genuine, routable IPv6 prefix alongside it — many are, because IPv6 addresses aren’t scarce and CGNAT for IPv6 defeats the entire point of the protocol’s address space. Check with:
| |
If that returns a real global unicast address (not fe80:: link-local, not a fd00::/8 unique-local range), the homelab has genuine inbound IPv6 reachability even with IPv4 fully CGNAT’d. A service listening on a public IPv6 address is reachable from any client that also has IPv6 — which, depending on the ISPs involved on both ends, might be everyone or might be nobody, since IPv6 adoption varies enormously by country and provider. It’s not a complete substitute for IPv4 reachability given how inconsistent client-side IPv6 support still is, but it’s a genuinely free, low-effort option worth checking before reaching for a tunnel, and it composes fine alongside one — dual-stack services happily serve both a tunnel-provided IPv4 path and a native IPv6 path simultaneously.
Troubleshooting
The WireGuard relay tunnel connects but traffic stops after a few minutes of idle. This is the PersistentKeepalive problem described above — the ISP’s CGNAT table entry for the homelab’s outbound UDP session has expired. Set PersistentKeepalive = 25 on the CGNAT’d peer specifically; the relay-side peer doesn’t need it since it’s never the one behind the restrictive NAT.
Confirming CGNAT versus a plain router misconfiguration. Compare the WAN IP shown in the router’s status page against an external “what’s my IP” lookup run from a device on that same network. If they differ and the router’s WAN address falls in 100.64.0.0/10, that confirms CGNAT. If the router’s WAN address matches the external lookup and port forwarding still fails, the more likely culprit is a local firewall rule or the ISP blocking specific inbound ports outright — 25 and 80 are common targets even on connections with a genuine public IP — so it’s worth ruling that out before building a whole tunnel architecture to fix what might be a one-line firewall rule.
A Cloudflare Tunnel handles HTTP/HTTPS cleanly and raw TCP services less so. Free-tier Cloudflare Tunnel is primarily built around HTTP(S) and a limited set of other protocols; arbitrary TCP/UDP tunnelling exists but carries more constraints and historically required a paid plan for some traffic types. For a raw TCP service — a game server, a non-HTTP protocol — a WireGuard relay through your own VPS is usually the more predictable option.
The relay VPS itself becomes a bottleneck under load. A single cheap vCPU handles WireGuard encryption for typical homelab traffic without issue, but if the relay is also carrying something bandwidth-heavy — large file transfers, video streaming out — the VPS’s own network cap, common on budget providers, becomes the limiting factor well before the CPU does. Check the provider’s actual bandwidth allowance and the advertised port speed both, before troubleshooting what looks like a tunnel performance problem.
Is it worth it
If port forwarding still works, none of this is necessary — check the WAN IP against an external lookup before assuming CGNAT is the problem, because a much simpler local firewall or ISP port block is more common than people expect. Once CGNAT is confirmed, though, there’s genuinely no local fix: no router setting and no ISP support call resolves it, short of paying for a static-IP business tier if the ISP even offers one. A relay — Tailscale’s hosted mesh, a self-hosted Headscale instance, a WireGuard tunnel to a cheap VPS, or a managed tunnel like Cloudflare’s — becomes the only remaining category of solution, and which one fits depends on whether the goal is reaching your own devices or exposing a service to the public.




