TCP tunneling, explained
"TCP tunnel" sounds like jargon. The idea under it is almost embarrassingly simple: a second connection that carries traffic in place of a first one. Whenever the first connection can't happen directly — because of a firewall, NAT, or missing public IP — the tunnel is the workaround.
The problem tunnels solve
Your laptop is on a home network. The router has one public IP. Thirty other devices share it. When you run a web server on localhost:3000, only devices on the same LAN can reach it. Someone on the internet can't open a TCP connection to your laptop because:
- Your laptop's IP is a private address (
192.168.x.x,10.x.x.x). Not routable on the internet. - Even if you forwarded port 3000 on the router, residential ISPs block common ports.
- You might be behind carrier-grade NAT, where you don't have a unique public IP at all.
The fix is to stop trying to accept inbound connections. Make an outbound one instead, and piggyback inbound traffic on it.
Forward vs reverse tunnels
Both involve a client, a server, and an encrypted channel between them. The difference is the direction of the traffic they carry.
Forward tunnel
Client wants to reach a service on the other side of the server. Traffic goes: you → server → destination. This is how SSH jump hosts work, and how corporate VPNs let you reach internal services.
Reverse tunnel
The internet wants to reach a service on the client. Client opens the outbound connection; server forwards inbound internet traffic down it. Traffic goes: visitor → server → you.
This is what "expose localhost" tools do. TezTun, ngrok, Cloudflare Tunnel — all reverse tunnels.
How a reverse tunnel actually moves bytes
A concrete walk-through, with TezTun specifically:
- You run
teztun tcp 3000. The CLI opens a pool of four WebSockets (tunable via--pool-size, capped at 8) towss://teztun.uz:8443. That's the tunnel. - The server assigns a subdomain (
a3f8k2x9.ttn.uz) and remembers the WebSocket belongs to it. - A visitor loads
https://a3f8k2x9.ttn.uz. Their browser connects to our server, not yours. - Our server reads the HTTP
Hostheader. It matches a live tunnel. It sends anOPENmessage down the WebSocket: "new connection, ID 42, please connect to your localhost:3000." - Your CLI opens a local TCP connection to
localhost:3000. - Bytes arriving from the visitor get wrapped in
DATAmessages on the WebSocket, unwrapped on your machine, and written to the local socket. Replies flow back the same way. - When the visitor closes, a
CLOSEmessage tears down that virtual stream. The WebSocket stays open for the next one.
Each WebSocket in the pool is a multiplexer: hundreds of simultaneous visitor connections share the four persistent pipes, load-balanced across them. This is why SPA asset bursts and HLS segment fanout stay snappy instead of serialising.
Everything above describes raw TCP mode (teztun tcp) — the tunnel ships bytes and stays out of the way. For web apps you probably want teztun http instead: the edge parses HTTP, injects canonical X-Forwarded-* headers, streams structured access logs, and unlocks the local request inspector, edge basic auth, and Host rewrite. TCP mode stays the right pick for Postgres, Redis, MCP-over-stdio, and anything non-HTTP. See the HTTP-mode reference for flags.
Why WebSocket?
A few reasons over raw TCP:
- It goes through corporate firewalls. Port 443 is almost universally open. Raw TCP on port 8443 is often blocked.
- TLS is free. The outer connection is HTTPS; the inner stream is encrypted by default.
- It survives HTTP proxies. Many office and coffee-shop networks force traffic through a proxy. WebSocket tunnels through it cleanly.
Protocol vs wrapper
Here's a nuance that trips people up. TCP is the transport. HTTP, HTTPS, WebSocket, and most app protocols run on top of TCP. When we say "TCP tunnel," we mean the tunnel carries arbitrary TCP bytes — it doesn't care whether those bytes are HTTP requests, PostgreSQL wire protocol, or your custom binary format.
This is why TezTun works for things that aren't websites: databases during development, game servers, MQTT brokers, SSH, anything that speaks TCP.
Things that are not tunneling but often get called that
- VPN. A VPN puts you on a virtual network. Similar in spirit, different in scope — a VPN gives you an IP on a private network; a tunnel forwards one specific port.
- Reverse proxy. A reverse proxy accepts requests and forwards them; it's half of a tunnel. The other half is the outbound-initiated leg.
- Port forwarding on a router. That's just NAT mapping, not a tunnel. Doesn't help you if you're behind CGNAT.
When tunnels are the wrong answer
- Production traffic. Put it on a real server.
- Very high throughput. The extra hop costs latency and potentially bandwidth.
- Security-critical internal services. Don't expose them publicly; put them on a VPN.
For everything else — dev environments, demos, webhook testing, IoT — tunnels are the right hammer.