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