| 1 | // Package tunnel is the "try Eleven in one command" transport: an outbound |
| 2 | // reverse tunnel that gives a locally-running instance a public |
| 3 | // https://try-<slug>.<domain> address, with no inbound ports, no DNS step, and |
| 4 | // no certificate work (the router's wildcard cert already covers the domain). |
| 5 | // |
| 6 | // The shape is deliberately simple — two WebSocket flavours and zero clever |
| 7 | // framing — so it stays understandable and maintainable by a human with an |
| 8 | // afternoon (CLAUDE.md: built to outlive AI): |
| 9 | // |
| 10 | // - One CONTROL connection per tunnel: the client dials |
| 11 | // wss://<gateway>/api/tunnel/register and holds it open. The gateway |
| 12 | // assigns the slug, registers the route, and later writes small JSON |
| 13 | // "dial" messages down it when the public side needs a connection. |
| 14 | // - One DATA connection per backend connection: for each "dial" the client |
| 15 | // opens wss://<gateway>/api/tunnel/conn/<id>, and both ends treat that |
| 16 | // WebSocket as a plain byte pipe (websocket.NetConn) — the gateway's |
| 17 | // reverse proxy speaks ordinary HTTP/1.1 through it to the client's local |
| 18 | // instance, which is what makes WebSockets (the chat!) work through the |
| 19 | // tunnel unmodified. |
| 20 | // |
| 21 | // The server stays blind here exactly as everywhere else: what crosses the |
| 22 | // tunnel is the same TLS-stripped HTTP the router already fronts for every |
| 23 | // instance — message bodies are ciphertext before they ever reach it. |
| 24 | package tunnel |
| 25 | |
| 26 | // ControlMsg is every message that crosses a control connection, in either |
| 27 | // direction. Type discriminates: |
| 28 | // |
| 29 | // client → gateway: "hello" (opens the exchange; Version is informative) |
| 30 | // gateway → client: "ready" (Host is the assigned public hostname) |
| 31 | // gateway → client: "dial" (open a data connection for ID) |
| 32 | // gateway → client: "error" (human-readable refusal, then close) |
| 33 | type ControlMsg struct { |
| 34 | Type string `json:"type"` |
| 35 | Version string `json:"version,omitempty"` |
| 36 | Host string `json:"host,omitempty"` |
| 37 | ID string `json:"id,omitempty"` |
| 38 | Error string `json:"error,omitempty"` |
| 39 | // Relay mode (docs/relay-mode.md): a client claiming a stable hostname |
| 40 | // sends hello with Mode "relay"; the gateway answers with a "challenge" |
| 41 | // carrying Nonce, and the client proves its keypair with a "claim" |
| 42 | // carrying PubKey + Sig over the nonce. Absent Mode means a trial — |
| 43 | // exactly the original flow, so older clients are unaffected. |
| 44 | Mode string `json:"mode,omitempty"` |
| 45 | Nonce string `json:"nonce,omitempty"` |
| 46 | PubKey string `json:"pubkey,omitempty"` |
| 47 | Sig string `json:"sig,omitempty"` |
| 48 | } |
| 49 | |
| 50 | // slugAlphabet is lowercase and unambiguous (no 0/o, 1/l/i) — these end up in |
| 51 | // hostnames people read out loud. |
| 52 | const slugAlphabet = "abcdefghjkmnpqrstvwxyz23456789" |
| 53 | |