If your home server sits behind CGNAT (carrier-grade NAT), the classic "forward port 22 on the router" trick just doesn't work — your ISP shares one public IP across many customers, so there's no port to forward. Add a phone as your only device (on-call, traveling, laptop dead) and reaching that box feels impossible.
It isn't. Here's the approach that actually holds up, and how to do the whole thing from a phone.
Why port forwarding fails on CGNAT
Port forwarding assumes you own the public IP on your router's WAN side. Under CGNAT you don't — the ISP NATs you again upstream. So even a perfect router config forwards a port that the outside internet can never route to. Dynamic DNS doesn't help either; it points at an address that isn't really yours.
The usual workarounds each have a cost:
- Rent a VPS + reverse SSH tunnel — works, but you're paying for and hardening another public box, and exposing SSH on it.
- Ask the ISP for a static/public IP — sometimes possible, often paid, sometimes flatly refused.
- Cloudflare Tunnel / ngrok — great for HTTP, awkward-to-paid for raw SSH.
The approach that scales down to a phone: a mesh VPN
Instead of poking a hole inward, put the server and your phone on the same private overlay network and let them find each other. A WireGuard-based mesh — Tailscale, or the self-hosted Headscale control server — does exactly this:
- Install the mesh client on the home server; it dials out to the coordination server, so CGNAT is irrelevant (outbound connections always work).
- Install the same mesh on your phone.
- Both get stable private IPs (e.g.
100.x.y.z). Now your phone canssh user@100.x.y.zas if you were on the LAN — encrypted, no ports exposed to the public internet.
No inbound firewall rules. Nothing listening on a public IP. SSH is only reachable to devices you've explicitly added to the tailnet, which is a real security win on its own.
Self-hosted vs hosted control plane
- Tailscale (hosted): fastest to set up, generous free tier.
- Headscale (self-hosted): you run the coordination server yourself — appealing if you want zero third-party dependency for your homelab. It speaks the same client protocol.
Either way the client side is identical, which matters for the phone step below.
Doing it from the phone
This is where most guides stop, because the phone side is genuinely fiddly: you end up juggling a separate VPN app, a keys app, and an SSH app, tabbing between them every time a connection drops.
A cleaner setup is a mobile SSH client with the mesh built in, so the tunnel comes up with the connection instead of as a separate step. I build one — TermAI — with Tailscale integrated, plus SFTP and an AI helper that suggests commands and waits for you to confirm before anything runs (on a production box, "read before you run" matters). But the pattern is what counts: keep the overlay network and the terminal in one place so a dropped mobile connection re-establishes both.
Checklist for the phone:
- Mesh client connected, phone shows a
100.xaddress - SSH key imported to the phone (not a password) — generate on the phone, copy the public half to the server's
authorized_keys - Test
ssh user@100.x.y.zon Wi-Fi, then again on cellular to confirm CGNAT is truly bypassed - Optional:
tmuxon the server so a flaky mobile link doesn't kill a long job
Hardening once it works
Because SSH is now only reachable over the tailnet, you can safely:
- Set
PasswordAuthentication noand rely on keys. - Restrict
sshdto listen on the mesh interface only. - Drop any public port-forward rules you were fighting with.
TL;DR
CGNAT breaks port forwarding because you don't own the public IP. A WireGuard mesh (Tailscale or self-hosted Headscale) sidesteps it entirely with outbound connections, gives your phone a stable private IP to the server, and removes SSH from the public internet in the process. Put the mesh and the terminal in the same mobile app and the whole thing works from a phone without app-juggling.
What's your CGNAT workaround — reverse tunnel, mesh VPN, or did you get a static IP out of your ISP?
Top comments (0)