WebSocket Protocol

NullBore uses a WebSocket-based relay protocol inspired by bore and chisel. This page documents the wire protocol for client implementors.

Overview

┌─────────┐    HTTPS     ┌──────────┐  Control WS  ┌────────┐
│ Internet │────────────→│  Server   │←────────────→│ Client │
│  Client  │             │  (relay)  │  Data WS     │        │
└─────────┘              └──────────┘              └────────┘
                              ↕
                         localhost:3000

Connection flow

  1. Client creates tunnel via POST /v1/tunnels
  2. Client opens control WebSocket: GET /ws/control?tunnel_id={id}
  3. Inbound HTTP request arrives at /t/{slug} or {slug}.tunnel.nullbore.com
  4. Server hijacks the inbound connection and reconstructs the HTTP request bytes
  5. Server sends notification on control WS: {"type":"connection","id":"<uuid>"}
  6. Client opens data WebSocket: GET /ws/data?id=<uuid>
  7. Server pipes the inbound connection ↔ data WebSocket bidirectionally

Control channel

GET /ws/control?tunnel_id={tunnel_id}
Authorization: Bearer nbk_your_key

The control channel is a long-lived WebSocket. The server sends JSON messages:

Connection notification

{"type": "connection", "id": "550e8400-e29b-41d4-a716-446655440000"}

Sent when a new inbound request arrives. The client must open a data WebSocket with this id within 10 seconds, or the connection is dropped.

Keepalive

The server sends WebSocket ping frames every 30 seconds. The client must respond with pong within 60 seconds or the connection is considered dead.

Data channel

GET /ws/data?id={connection_id}

The data channel is a short-lived WebSocket for a single connection. Once opened:

  1. Server writes the reconstructed HTTP request bytes (method, path, headers, body)
  2. Bidirectional io.Copy streams bytes between the inbound client and the data WebSocket
  3. When either side closes, the other side closes too

Data flows as raw WebSocket binary messages — no JSON wrapping, no framing.

WSNetConn adapter

The server wraps websocket.Conn as a standard net.Conn via a WSNetConn adapter. This allows using standard Go io.Copy for the relay, with proper Close, Read, and Write semantics.

Timeouts

TimeoutDurationDescription
Pending connection10sTime for client to open data WS after notification
Ping interval30sServer sends ping on control channel
Pong timeout60sClient must respond to ping