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:

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:

  1. You run teztun tcp 3000. The CLI opens a pool of four WebSockets (tunable via --pool-size, capped at 8) to wss://teztun.uz:8443. That's the tunnel.
  2. The server assigns a subdomain (a3f8k2x9.ttn.uz) and remembers the WebSocket belongs to it.
  3. A visitor loads https://a3f8k2x9.ttn.uz. Their browser connects to our server, not yours.
  4. Our server reads the HTTP Host header. It matches a live tunnel. It sends an OPEN message down the WebSocket: "new connection, ID 42, please connect to your localhost:3000."
  5. Your CLI opens a local TCP connection to localhost:3000.
  6. Bytes arriving from the visitor get wrapped in DATA messages on the WebSocket, unwrapped on your machine, and written to the local socket. Replies flow back the same way.
  7. When the visitor closes, a CLOSE message 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.

TCP vs HTTP mode

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:

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

When tunnels are the wrong answer

For everything else — dev environments, demos, webhook testing, IoT — tunnels are the right hammer.

Try a tunnel
Free through December 31, 2027. New accounts receive Max access.
Download TezTun