Skip to content

GoTunnel Architecture

A conceptual overview of how GoTunnel exposes a local service to the public internet. This document is for people who want to understand how the tool works before trusting it with their traffic. It describes behaviour, not internal source layout.

High-level overview

GoTunnel is a reverse TCP tunnelling system. A client running on your machine keeps a single outbound connection open to a server on the public internet. Public visitors reach the server, and the server forwards their traffic down that connection to your local service.

+----------------+        +-----------------+        +----------------+
|  Public        |        |  GoTunnel       |        |  GoTunnel      |
|  visitors      | -----> |  server         | <----- |  client        |
|  (browser,     |        |  (public host)  |  one    |  (your machine)|
|   curl, etc.)  |        |                 |  conn   |                |
+----------------+        +-----------------+        +--------+-------+
                                                              |
                                                              v
                                                     Local service
                                                     (localhost:3000, ...)

Because it operates at the TCP layer, the tunnel is protocol-agnostic: HTTP, gRPC, WebSockets, databases, and SSH all work without modification.

Two ways to expose a service

Mode How a visitor reaches you When the server uses it
Subdomain (HTTP) https://<random>.<base-domain> routed by the Host header Server started with --base-domain (the managed host uses this)
Port tcp://<server>:<assigned-port> Default self-hosted mode (no --base-domain)

In subdomain mode the client also connects over WebSocket (wss) on port 443, so it works from restrictive networks and reuses a normal TLS certificate. In port mode the client speaks the raw framed protocol directly over TCP (optionally wrapped in TLS).

Connection lifecycle

  1. Server starts and listens for tunnel connections.
  2. Client dials the server (with automatic retry and exponential backoff).
  3. Client sends a handshake declaring its local address and a device id.
  4. Server acknowledges the handshake.
  5. Client authenticates with a token.
  6. Server validates the token (constant-time compare against hashed tokens) and, on repeated failures from one IP, temporarily locks that IP out.
  7. Server assigns a public endpoint (a port, or a random subdomain) and, only after the public listener has successfully bound, replies with BindOK.
  8. The tunnel is active. Each public connection becomes a multiplexed stream.
  9. Streams carry data in both directions until closed.
  10. Heartbeats keep the session alive; a session that goes silent past the timeout is expired.
  11. On Ctrl-C or q, the client shuts down cleanly and prints a metrics summary.

Stream multiplexing

Every public connection maps to one stream, identified by a 32-bit stream id carried in each frame header. Many streams share the single tunnel connection concurrently, and one stream failing does not affect the others. Stream id 0 is reserved for control frames (handshake, auth, heartbeat, and so on).

Visitor A ─┐
Visitor B ─┼─► server ──(stream 1,2,3 over one tunnel)──► client ──► local service
Visitor C ─┘

Beta access (managed server)

The managed server gates the beta by device. The first time you connect from a device with no token, the client asks for your Gmail address and the server emails a one-time code; entering it signs the device in. Under the hood:

  • The email must be a real @gmail.com address, aliases (+tag) are rejected, and dots are normalised, so one person maps to one account. Access is limited to one device per email.
  • On success the server issues an HMAC-signed, device-bound token with an expiry. The token is stored on your device and presented on every connect; the tunnel server verifies the signature locally, so normal connections don't hit the accounts database.
  • One-time codes live briefly in Redis with a resend cooldown and an attempt limit; enrolled testers are recorded in Postgres.

Self-hosters skip all of this and use static tokens (--auth-tokens / GOTUNNEL_AUTH_TOKENS) instead.

Security model

GoTunnel is built so that the public-facing surface is the untrusted side.

  • Token authentication — the server requires one or more tokens (--auth-tokens / GOTUNNEL_AUTH_TOKENS). Tokens are compared as SHA-256 hashes using a constant-time comparison and are never written to logs. There is no default token; a server with no tokens configured refuses to start.
  • Brute-force protection — repeated failed authentications from the same IP trigger a temporary lockout (--max-auth-failures, --auth-lockout).
  • Transport encryption — TLS 1.2+ is supported on the raw TCP control channel (--tls), and the managed subdomain mode terminates real TLS at the edge. Running without transport encryption is possible for local development but warned against for production.
  • Optional mTLS — the server can require client certificates signed by a given CA (--tls-client-ca), and the client can present a certificate (--tls-cert / --tls-key) for stronger client identity.
  • Configurable TLS server name — the client verifies the server certificate against a configurable name (--tls-servername), defaulting to the --server host.
  • Deadlines and limits — public-facing reads have a first-byte deadline to blunt slowloris-style abuse, writes have deadlines to avoid hung connections, sessions have a maximum duration and an idle timeout, and the server can cap the number of concurrent tunnels (--max-connections).
  • Session isolation — one client cannot see another client's streams; each session has its own stream namespace, endpoint, and metrics.
  • Frame limits — a maximum frame size bounds memory per read and rejects oversized payloads.

Reliability

  • Heartbeats run on both ends; a watchdog expires a session that stops responding.
  • Auto-reconnect on the client re-establishes the tunnel after transient network loss, using exponential backoff (1s → 30s), unless --no-reconnect is set.
  • Graceful shutdown closes public listeners, drains streams, and releases ports on disconnect and on server shutdown.

Observability

The client shows a live terminal UI with the forwarding endpoint, latency (measured via periodic ping/pong), uptime, TLS/reconnect state, and a colour-coded, timestamped HTTP request log. The server logs session lifecycle events, authentication outcomes (never the token), endpoint assignments, and per-request HTTP summaries. A basic /healthz endpoint is available for monitoring.

Updating

The client can update itself in place:

gotunnel update

It checks the latest published release, downloads the correct build for your platform, and swaps the binary.