Skip to content

GoTunnel Protocol Specification

GoTunnel uses a binary, framed, stateful protocol over TCP (optionally TLS) or over a WebSocket carrying binary messages. This document describes protocol version 1.

Transport

  • Transport: TCP, optionally wrapped in TLS 1.2+, or a WebSocket (binary frames) for the subdomain/wss path.
  • Encoding: binary, big-endian.
  • The tunnel payload itself is opaque application data, so any TCP-based protocol is carried unchanged.

Frame format

Every message is a frame with a fixed 10-byte header followed by a payload.

+---------+--------+-----------+-------------+-------------+
| Version | Type   | Stream ID | Payload Len | Payload     |
| 1 byte  | 1 byte | 4 bytes   | 4 bytes     | N bytes     |
+---------+--------+-----------+-------------+-------------+
Field Size Description
Version 1 byte Protocol version (0x01)
Type 1 byte Message type (see below)
Stream ID 4 bytes Stream identifier; 0 for control frames
Payload Len 4 bytes Payload length in bytes
Payload N bytes Message payload

Frames whose declared payload length exceeds the maximum (16 MB) are rejected. Frames with an unknown version are rejected and the connection is closed.

Message types

Type Value Direction Description
MsgHandshake 1 client → server Handshake request
MsgHandshakeAck 2 server → client Handshake acknowledgment
MsgAuth 3 client → server Authentication token
MsgAuthOK 4 server → client Authentication accepted
MsgAuthErr 5 server → client Authentication rejected
MsgBindOK 6 server → client Public endpoint assigned
MsgStreamOpen 7 server → client Open a new stream
MsgStreamData 8 both Stream data
MsgStreamClose 9 both Close a stream
MsgHeartbeat 10 both Keepalive
MsgError 11 both Error / session rejected (payload is a message)
MsgPing 12 client → server Latency probe (payload: timestamp)
MsgPong 13 server → client Latency reply (echoes the ping payload)

Session state machine

The server enforces ordering on inbound frames. Out-of-order frames are rejected and the session is closed.

INIT ──MsgHandshake──► HANDSHAKEN ──MsgAuth──► AUTHENTICATED ──► FORWARDING
State Accepted next Result
INIT MsgHandshake → HANDSHAKEN (anything else → error)
HANDSHAKEN MsgAuth → AUTHENTICATED (anything else → error)
AUTHENTICATED / FORWARDING stream + control frames stays forwarding

After authentication the server assigns the public endpoint and sends MsgBindOK, moving the connection into active forwarding.

Flow

1. Handshake

Client → server MsgHandshake. Payload:

+--------+---------------+------------+---------------+-----------+--------------+----------+
| Role   | Capabilities  | ExposeLen  | Expose Addr   | DeviceLen | Device ID    |          |
| 1 byte | 8 bytes       | 2 bytes    | var           | 2 bytes   | var          |          |
+--------+---------------+------------+---------------+-----------+--------------+----------+
  • Role: client (0x01) or server (0x02).
  • Capabilities: 64-bit feature bitmask (heartbeat, compression, reconnect, metrics; reserved bits for future use).
  • Expose Addr: the client's local service address, e.g. localhost:3000.
  • Device ID: a stable client identifier (used for single-session enforcement).

Server → client MsgHandshakeAck (no payload).

2. Authentication

Client → server MsgAuth, payload is the token as raw UTF-8 bytes.

Server → client MsgAuthOK (no payload) on success, or MsgError with a human-readable message on failure. The token is validated with a constant-time comparison against the server's configured token hashes; it is never logged. Repeated failures from one IP produce a temporary lockout, after which the server replies with MsgError describing the retry delay.

3. Bind

Server → client MsgBindOK, sent only after the public listener has bound. Payload:

+-------------+------------------+
| Public Port | Host (optional)  |
| 2 bytes     | var (UTF-8)      |
+-------------+------------------+
  • Port mode: port is the assigned public port, host is empty.
  • Subdomain mode: port is 0, host is the assigned <random>.<base-domain>.

4. Heartbeat and latency

MsgHeartbeat (no payload) is sent periodically by both ends; a session that receives no frames within the timeout is expired. The client also sends MsgPing carrying an 8-byte timestamp; the server echoes it back as MsgPong, letting the client display round-trip latency.

5. Stream lifecycle

  • MsgStreamOpen (server → client): a public connection arrived; the stream id in the header identifies it. The client dials the local service.
  • MsgStreamData (both): raw application bytes for that stream, up to 16 MB per frame.
  • MsgStreamClose (both): one side finished; the peer drains and releases the stream.

Each public connection is exactly one stream. Stream ids are assigned sequentially by the server starting at 1.

Limits

Constraint Value Notes
Max payload size 16 MB Per frame
Frame header 10 bytes Fixed
Stream id range 1 – 2³²−1 0 reserved for control
Heartbeat interval 10s
Heartbeat timeout 30s Session expires after this
Connect timeout 10s Initial dial
Reconnect backoff 1s → 30s Exponential

Errors

MsgError carries a UTF-8 message and is used for authentication failure, lockout, capacity rejection, and session time limits. Invalid frames, unknown versions, oversized payloads, and out-of-order control frames all terminate the session.