Tobs

Docs

Private relay protocol

Protocol and conformance checks for compatible relays.

The private relay standard lets one outbound-only service carry the encrypted phone channel for several computers that have no inbound reachability. Anyone can implement a compatible relay; the bundled Docker relay in deploy/private-relay is the reference implementation. A relay is a dumb carrier: it never sees plaintext, and it never decides who controls a computer. The computer's pinned Noise key and its exact-device approval still decide that.

This page describes botharness-private-v2. A v2 relay also accepts every v1 request, so a single-computer v1 setup keeps working.

Roles and secrets

  • Claim key: 64 lowercase hex characters, one per computer, used once. The operator creates it on the relay side and gives it privately to one computer. The v1 setup key (bootstrap.secret) is the claim key of the computer named default.
  • Owner token and phone token: 64 lowercase hex characters each, generated by the computer at claim time. The owner token opens the computer's control socket. The phone token is placed in the pairing code and admits phones to that computer only.
  • The relay stores only SHA-256 hashes of claim keys, owner tokens and phone tokens, and compares them in constant time. Owner and phone tokens must be unique across the whole deployment, because the relay finds the computer from the token alone.

HTTP endpoints

All endpoints are HTTPS in production. Responses are JSON with cache-control: no-store.

Request Auth Result
GET /health none 200 {"protocol":"botharness-private-v1","protocols":["botharness-private-v1","botharness-private-v2"]}
POST /claim with body {"ownerToken":"<64 lowercase hex>","phoneToken":"<64 lowercase hex>"} (≤1 KiB) Authorization: Bearer <claim key> 200 {"claimed":true,"host":"example-host"}; 403 unknown key; 409 key already used (also after release); 400 malformed or reused tokens
DELETE /claim Authorization: Bearer <owner token> 200 {"released":true}: closes that computer's sockets and forgets its tokens; the claim key stays used
GET /status Authorization: Bearer <owner token> 200 {"connected":true,"host":"example-host"}; 404 otherwise

Claims are exclusive per computer: of two competing claims with the same key exactly one gets 200, and every later claim with that key gets 409, including after a relay restart.

WebSocket endpoints

  • GET /host (upgrade), Authorization: Bearer <owner token>: the computer's control socket. At most one per computer; a second one is refused with 403.
  • GET /botharness-device/channel (upgrade), phone token in Authorization: Bearer or X-BotHarness-Admission: a phone channel. Refused with 403 when the token is unknown, the computer's control socket is not open, or that computer already has 32 phone channels.

A phone channel is routed only to the computer that owns its phone token. Frames are text, at most 70,000 bytes, and opaque to the relay (they are Noise ciphertext).

Control socket messages

JSON text frames, id is 32 lowercase hex characters chosen by the relay per phone channel.

  • relay → computer: {"type":"open","id":"0123456789abcdef0123456789abcdef"} when a phone connects; {"type":"data","id":"0123456789abcdef0123456789abcdef","data":"<Noise ciphertext>"} for each phone frame; {"type":"close","id":"0123456789abcdef0123456789abcdef"} when the phone leaves.
  • computer → relay: {"type":"data","id":"0123456789abcdef0123456789abcdef","data":"<Noise ciphertext>"} to send a frame to that phone; {"type":"close","id":"0123456789abcdef0123456789abcdef"} to close it.

When the control socket closes, the relay closes all of that computer's phone channels and none of any other computer's. Any malformed control frame closes the control socket. The relay pings every 25 seconds and drops sockets that do not answer, and drops any socket whose send queue exceeds 2 MiB.

The ID above is an example; <Noise ciphertext> stands for the actual encrypted frame, not a literal message to send.

Conformance

deploy/private-relay/conformance.mjs checks a running relay from the outside:

cd deploy/private-relay && npm ci
node conformance.mjs --origin https://relay.example.com --key-a-file a.secret --key-b-file b.secret

Use two unused claim keys. The run claims both, connects two computers and their phones, checks that each phone reaches only its own computer, that one computer going offline or releasing its claim leaves the other untouched, and that a released key cannot be claimed again. It ends with conformance: pass or lists each failed check.

View the source on GitHub