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, ...)
The tunnel itself carries raw bytes. On the hosted server, HTTP tunnels (gotunnel http) carry HTTP traffic to a public https URL, and TCP tunnels (gotunnel tcp) carry anything else that runs over TCP, such as databases and SSH, locked to approved IPs.
Three 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 hosted server uses this) |
| TCP tunnel | <name>.<tcp-base-domain>:<assigned-port>, routed by port, locked to approved IPs |
gotunnel tcp <port> on a server with GOTUNNEL_TCP_ENABLED=true |
| Port | tcp://<server>:<assigned-port> |
Default self-hosted mode (no --base-domain) |
Raw TCP carries no hostname, so a TCP tunnel is routed by its own public port from a small range. The name is only a friendly label. Every TCP tunnel starts locked: the server refuses any connection whose source IP has not been approved, before a stream is opened or any byte reaches your machine. An IP is approved in one of two ways:
--allowliston the owner's command line.- Approval link: the owner shares a link. The person connecting opens it on the network they will connect from and verifies their email with a one-time code. Their current IP is then approved until the tunnel expires, and the owner sees the approval live.
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¶
- Server starts and listens for tunnel connections.
- Client dials the server (with automatic retry and exponential backoff).
- Client sends a handshake declaring its local address and a device id.
- Server acknowledges the handshake.
- Client authenticates with a token.
- Server validates the token (constant-time compare against hashed tokens) and, on repeated failures from one IP, temporarily locks that IP out.
- Server assigns a public endpoint (a port, or a random subdomain) and, only after the public listener has successfully bound, replies with
BindOK. - The tunnel is active. Each public connection becomes a multiplexed stream.
- Streams carry data in both directions until closed.
- Heartbeats keep the session alive; a session that goes silent past the timeout is expired.
- 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 ─┘
Device registration (hosted server)¶
The hosted server registers each device once. 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.comaddress, 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, tied to the server that issued it, and presented on every connect. The tunnel server verifies the signature and checks that the token is still the one on record for the device, so released or replaced tokens stop working. If that check can't run, the connection is refused and the client can try again.
- One-time codes live briefly in Redis with a resend cooldown and an attempt limit, and emails and code attempts are limited per address and per client. Responses are the same whether or not an address is already registered or suspended; the explanation goes to that inbox. Registered devices are recorded in Postgres, and an expired registration no longer blocks a new one.
gotunnel uninstallfrees the registration so the email can be used again. The client sends its device ID and current token, and the server deletes the record only if that token matches the one it last issued to the device.
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 within the
--auth-lockoutwindow trigger a temporary lockout (--max-auth-failures,--auth-lockout). A successful login does not clear earlier failures. - Transport encryption — TLS 1.2+ is supported on the raw TCP control channel (
--tls), and the hosted 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. This needs--tlsand a disabled WebSocket listener (--ws-addr=), and the server refuses to start otherwise. - Configurable TLS server name — the client verifies the server certificate against a configurable name (
--tls-servername), defaulting to the--serverhost. - Deadlines and limits — connections must authenticate within 15 seconds and are capped per IP until they do. Public-facing reads have a first-byte deadline to blunt slowloris-style abuse, control and public 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). Closing a session always closes its connection. - 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, with a lower limit before authentication.
- Locked TCP tunnels — unapproved connections are dropped before a stream opens. Per-IP connection rate limits, a per-tunnel connection cap, one TCP tunnel per account, and a fixed session length bound what an approved IP can do. IPs that repeatedly hit tunnels without approval are temporarily banned.
- Kernel gating (optional) — with nftables, the TCP port range is closed in the kernel except for approved (IP, port) pairs, so scanners do not even see the port. Entries expire with the tunnel and are flushed when the server starts.
- Operator controls — operators can list and close tunnels and ban accounts through a local, owner-only admin socket. Bans block both connecting and registering. Abuse patterns trigger email alerts.
- Audit metadata — for TCP tunnels the server records sessions, approvals and connections (who, when, which IP, how many bytes), never the traffic itself, and deletes them after a retention period (90 days on the hosted server).
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 with jitter (1s → 30s). It keeps trying until the server is reachable again, unless
--no-reconnectis set or--max-retriescaps the attempts. - 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.