Skip to main content

Exposing localhost with a tunnel

Summary: a machine behind NAT has no public address, so nothing on the internet can call into it โ€” which blocks any webhook you want to develop against. A tunnel client makes an outbound connection to a provider that owns a public domain, and the provider forwards inbound requests back down it.

How a tunnel worksโ€‹

caller โ†’ https://xxxx.trycloudflare.com โ†’ tunnel client โ†’ localhost:8000
(provider owns the domain (running on (your server)
and its certificate) your machine)
  • The connection is outbound. Your machine dials the provider, and traffic flows back down that existing connection โ€” so no port-forward, no inbound firewall rule, and NAT is irrelevant.
  • You need no certificate. The provider owns the domain and terminates TLS at its edge; your app serves plain HTTP locally.
  • Free URLs are ephemeral. They're regenerated on every restart, so anything registered against them (a webhook config) must be re-pointed each session. A named tunnel on your own domain gives a fixed URL.

Ports are global standards, not local choicesโ€‹

Port numbers are assigned by IANA, the body that manages internet number standards. A port is a promise about what's listening, understood by every machine on the internet.

  • 80 HTTP ยท 443 HTTPS ยท 22 SSH ยท 5432 PostgreSQL โ€” registered, universal.
  • 8000 โ€” developer convention only. Nothing special; 8001 or 9000 behave identically.
  • 443 is effectively unblockable. Blocking it breaks every website for the whole building, so corporate firewalls leave it open. Anything that needs to cross one aims for 443 โ€” which is also why "it works on 443" is no evidence that a protocol is allowed.

What tunnel clients actually use:

  • cloudflared โ€” QUIC over UDP 7844, falling back to HTTP/2 over TCP 7844 if UDP is blocked. Same port, not 443; 443 is only used for some optional features. Corporate networks commonly block outbound UDP while allowing TCP, which is what the fallback is for (docs).
  • ngrok โ€” outbound TLS on 443, like any HTTPS request. Its :4040 is the local inspector UI on 127.0.0.1 and never crosses the network (docs).

When a tunnel fails on a corporate network, ports are rarely the whole story. Check whether TLS interception is breaking the client's own connection (see certificate trust), and whether the provider's domain is category-blocked as an anonymiser โ€” both are common, and neither shows up as a port problem.

Where this came upโ€‹

Shortcut webhooks needed to reach a FastAPI server on a laptop. ngrok didn't work on the agency network and cloudflared did; the cause was never established โ€” on ports alone ngrok should have passed, so TLS interception or domain blocking are the likely suspects.

Daily startup for that project:

docker-compose up -d
& "$env:TEMP\cloudflared.exe" tunnel --url http://localhost:8000
# copy the new URL into Shortcut โ†’ Settings โ†’ Webhooks โ†’ https://xxxx.trycloudflare.com/webhook
curl https://xxxx.trycloudflare.com # expect {"detail":"Not Found"}

Not the same problem as SSL_VERIFYโ€‹

Certificate trustTunnel
DirectionOutbound โ€” your app calling outInbound โ€” outside calling in
ProblemProxy intercepts and re-signs your trafficMachine has no public address
Whose certificateThe proxy's โ€” causes the problemThe provider's โ€” solves the problem
FixTrust the CA (SSL_VERIFY=false in dev only)Run a tunnel client