| 1 | # 🤖 `lchat try` — the one-command trial tunnel |
| 2 | |
| 3 | `lchat try` is the zero-setup way to experience a real, self-run Eleven |
| 4 | space: download the binary, run one command, get a public |
| 5 | `https://try-<slug>.11ch.at` address serving a full instance from your own |
| 6 | machine — through NAT, with no inbound ports, no DNS record, no certificate, |
| 7 | and no root. Stop the command and the address retires; the space's data stays |
| 8 | in `./lchat-try.db`. |
| 9 | |
| 10 | ```sh |
| 11 | curl -LO https://github.com/elevenmessenger/releases/releases/download/server/lchat-linux-amd64 |
| 12 | chmod +x lchat-linux-amd64 |
| 13 | ./lchat-linux-amd64 try |
| 14 | ``` |
| 15 | |
| 16 | It prints two links: the space's address, and a one-time setup link that makes |
| 17 | you its admin (the same conversational setup a hosted space gets). |
| 18 | |
| 19 | ## How it works |
| 20 | |
| 21 | Two halves, both in the single binary (`internal/tunnel/`): |
| 22 | |
| 23 | - **The client** (`lchat try`) starts a normal local instance (a subprocess |
| 24 | running the ordinary single-instance path against `lchat-try.db`), then |
| 25 | dials out to the gateway over one **control WebSocket** |
| 26 | (`wss://tunnel.11ch.at/api/tunnel/register`). The gateway assigns |
| 27 | `try-<slug>.11ch.at` and sends `{"type":"ready","host":…}`. For every |
| 28 | backend connection the public side needs, the gateway sends |
| 29 | `{"type":"dial","id":…}` and the client dials back one **data WebSocket** |
| 30 | (`/api/tunnel/conn/<id>`), which both ends treat as a plain byte pipe |
| 31 | (`websocket.NetConn`) spliced to the local instance's TCP port. Ordinary |
| 32 | HTTP/1.1 flows through it — including WebSocket upgrades, which is what |
| 33 | makes the chat itself work through the tunnel unmodified. |
| 34 | - **The gateway** (`lchat tunnel serve`, unit `lchat-tunnel.service`) listens |
| 35 | on `/run/lchat/tunnel.sock` behind the ordinary router. Each live tunnel is |
| 36 | published as a plain registry route (`kind=service`, `host=try-…`, |
| 37 | `socket=/run/lchat/tunnel.sock`), so the router needs no tunnel awareness at |
| 38 | all: TLS comes from the existing `*.11ch.at` wildcard cert, and routing is |
| 39 | the same host→socket lookup every instance uses. The router notices new |
| 40 | routes via its registry watcher (a `PRAGMA data_version` poll every 2s), so |
| 41 | the unprivileged gateway never needs rights to signal anything. |
| 42 | |
| 43 | The E2EE story is unchanged: what crosses the tunnel is the same |
| 44 | already-ciphertext traffic the router fronts for every instance. The gateway |
| 45 | can see exactly what the router can see — nothing readable. |
| 46 | |
| 47 | ## Why single-label slugs |
| 48 | |
| 49 | `try-x7k2m.11ch.at`, not `x7k2m.try.11ch.at`: TLS wildcards match one label, |
| 50 | and the fleet's wildcard cert is `*.11ch.at`. A nested scheme would need its |
| 51 | own wildcard cert and DNS; the prefix gets the same effect for free. |
| 52 | |
| 53 | ## Operations |
| 54 | |
| 55 | - **Enable on a box:** `systemctl enable --now lchat-tunnel`, then route its |
| 56 | control host once: `lchat add-service tunnel.11ch.at --socket |
| 57 | /run/lchat/tunnel.sock`. (Bootstrap installs the unit everywhere; it's inert |
| 58 | until enabled.) |
| 59 | - **Config** (`/data/tunnel.env`, optional): `LCHAT_TUNNEL_HOST`, |
| 60 | `LCHAT_TUNNEL_DOMAIN`, `LCHAT_TUNNEL_SOCKET`, plus `--max` (default 50 |
| 61 | concurrent tunnels). |
| 62 | - **Guardrails:** 5 new tunnels per IP per hour, 50 concurrent tunnels, 32 |
| 63 | concurrent connections per tunnel, 24-hour lifetime, dial-back timeout 15s. |
| 64 | And the door check: before a hostname goes public — and every 10 minutes |
| 65 | after — the gateway probes the tunnelled backend through the tunnel |
| 66 | (`GET /api/version`) and refuses anything that isn't an Eleven instance, |
| 67 | so the trial domain never lends its name (or its TLS) to arbitrary |
| 68 | content. This is a trial surface, not free hosting — the limits say so |
| 69 | politely. |
| 70 | - **Kill switch:** `lchat tunnel kill try-<slug>.11ch.at` removes the route |
| 71 | (the router stops serving it within seconds); `systemctl restart |
| 72 | lchat-tunnel` additionally drops every live tunnel connection. |
| 73 | - **Cleanup is automatic:** a tunnel's route is removed when its client |
| 74 | disconnects; on gateway restart, leftover `try-%` rows pointing at its |
| 75 | socket are swept. `lchat instances` will show live trials as `service` |
| 76 | rows — transient by design. |
| 77 | - **Self-hosters** can run their own gateway anywhere the router fronts a |
| 78 | wildcard domain; `lchat try --gateway https://tunnel.your.domain` points the |
| 79 | client at it. |
| 80 | |
| 81 | ## Honest limits |
| 82 | |
| 83 | - A trial's address dies with the process (or at 24h). `lchat try` again |
| 84 | mints a fresh address; the data in `lchat-try.db` persists, but members' |
| 85 | saved links won't follow it. The printed copy says so — a keeper space |
| 86 | should graduate to a real install (`docs/self-hosting.md`). |
| 87 | - The gateway is a relay for one machine's trial, not a CDN: throughput is |
| 88 | whatever the trial machine's uplink gives it. |
| 89 | |