Documentation
kino-relay
The public switchboard. Endpoints, deployment behind nginx, TLS, authentication modes, and self-enrollment.
Repository: Samarthegde/kino-relay · Licence: AGPL-3.0 · Language: Rust
The one publicly reachable piece. Agents dial out to it and park; the manager asks it to open a session; it splices the two sockets and forwards bytes. It holds no state on disk (beyond a cached public key), terminates no SSH, and never sees your credentials.
Endpoints#
| Route | Who connects | Purpose |
|---|---|---|
GET /healthz |
monitors, controllers | Liveness. Returns ok. |
GET /ws/control?agent_id=<id> |
agent | The parked channel. Kept alive with 30-second pings. |
GET /ws/manager/request?agent_id=<id> |
manager | Asks to reach an agent. The relay notifies it and waits up to 10 seconds for the dial-back. |
GET /ws/agent/data?session_id=<id> |
agent | The return data socket for one session. |
When auth is configured, every /ws/* route requires an Authorization:
Bearer header — a header rather than a query parameter, so tokens stay out
of proxy access logs.
Quick start#
cargo run --release
# Relay listening on 0.0.0.0:3000 (plaintext - terminate TLS at your proxy)
kino-agent --relay-url ws://localhost:3000 --agent-id test
Configuration#
| Env var | Default | Description |
|---|---|---|
PORT |
3000 |
Port to listen on. |
BIND |
0.0.0.0:$PORT |
Full listen address; overrides PORT. |
TLS_CERT / TLS_KEY |
unset | PEM chain + key. Set both to serve wss:// directly. |
RELAY_TOKEN |
unset | Static bearer token. See Authentication. |
RELAY_JWT_PUBLIC_KEY |
control.pub.pem when enrolling |
Path to the controller’s Ed25519 public key. Loaded if present, written on enrollment. |
KINO_CONTROL_URL |
unset | Controller to enroll with. |
KINO_ENROLL_CODE |
unset | One-time enrollment code. |
RELAY_PUBLIC_URL |
unset | The address clients dial, e.g. wss://relay.example.com. Required to enroll. |
RELAY_NAME |
unset | Display name in the controller’s relay list. |
RUST_LOG |
info |
Log verbosity. |
SIGTERM and SIGINT are handled gracefully, so docker stop and systemd
restarts do not cut sessions mid-frame.
Deployment#
The relay speaks plain HTTP by default and expects TLS terminated in front of
it so clients can use wss://.
Docker Compose#
cp .env.example .env # fill in enrollment vars and/or RELAY_TOKEN
docker compose up -d --build
LISTEN_ADDR chooses the interface (127.0.0.1 when nginx is on the same box,
0.0.0.0 or a LAN address when it is not) and RELAY_PORT the published port.
The container always listens on 3000 internally. A relay published beyond
loopback should be firewalled to the proxy’s IP.
The relay-data volume persists control.pub.pem, so restarts keep enforcing
auth without a fresh enrollment code.
nginx#
WebSocket upgrade headers are mandatory — without them every connection
fails with 400 Bad Request. A complete server block ships in the repository
at deploy/nginx-kino-relay.conf; the parts that matter:
# at http{} level, once:
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 1h; # don't cut idle SSH sessions
proxy_buffering off;
}
Raise the read timeout
proxy_read_timeout defaults to 60 seconds, which kills an idle terminal
after a minute. The relay pings the control channel, but a data socket
has no such keepalive. If you are also routing through an nginx stream
block, set proxy_timeout 1h; there too — its default is 10 minutes.
Direct TLS, no proxy#
Set both TLS_CERT and TLS_KEY and the relay serves wss:// itself, and
refuses plaintext. ALPN is pinned to HTTP/1.1, which is what WebSockets need.
TLS_CERT=/path/fullchain.pem TLS_KEY=/path/privkey.pem PORT=443 kino-relay
Where to host it#
Tiny and stateless, but it holds long-lived WebSocket connections, so it must run somewhere that does not idle-sleep or cap connection duration. An always-on VM is the right shape — Oracle Cloud’s Always Free ARM tier works well, and the aarch64 build covers it. Serverless and scale-to-zero tiers do not work.
Careful with CDN proxies
Putting a relay behind Cloudflare’s orange cloud adds a ~100-second idle WebSocket timeout and puts long-lived non-HTTP tunnelling in the way of the terms of service. Grey-cloud the record, or point clients straight at the origin.
Installing#
Prebuilt Linux binaries (x86_64 and aarch64) hang off every release:
curl -fsSL -o kino-relay \
https://github.com/Samarthegde/kino-relay/releases/latest/download/kino-relay-x86_64-unknown-linux-gnu
chmod +x kino-relay && sudo mv kino-relay /usr/local/bin/
Building needs stable Rust ≥ 1.85. TLS is rustls with the ring backend — no
OpenSSL dependency.
Authentication#
Two mechanisms, independently optional. A caller passes if it satisfies either.
| Env var | Mechanism |
|---|---|
RELAY_TOKEN |
One static shared secret, compared in constant time. The simple option for a personal relay: the same string goes here, in kino-agent --token, and in the host’s Relay token field in the manager. |
RELAY_JWT_PUBLIC_KEY |
Path to an Ed25519 public key. Verifies JWTs minted by a controller. Tokens carry a role (agent / manager) and an agent_id scope, so a manager token for host A cannot register agents or reach host B. |
Verification is asymmetric on purpose: the relay holds no signing material, so an operator can check credentials but never mint them. That is the property that makes a pool of community-run relays possible.
Neither variable set means the relay runs open
It says so loudly at startup. Anyone who can reach it and knows an
agent_id can open an SSH transport to that agent’s sshd, whose own
authentication becomes the only gate. Fine for a firewalled lab; set a
token for anything public.
Enrolling with a controller#
Enrollment puts the relay in the controller’s directory, so agents discover it and park on whichever enrolled relay answers fastest. The relay enrolls itself, two ways.
Interactive — run it in a terminal with no auth configured and it asks:
No auth is configured (RELAY_TOKEN / RELAY_JWT_PUBLIC_KEY unset).
Enroll this relay with a kino-control instance? [y/N]
Env-driven — for docker and systemd, where there is no TTY:
KINO_CONTROL_URL=https://kino.dpdns.org \
KINO_ENROLL_CODE=<one-time code> \
RELAY_PUBLIC_URL=wss://relay.example.com \
RELAY_NAME=eu-1 \
kino-relay
Get the one-time code from the Relays page.
Either way the relay starts serving first — the controller probes
/healthz before accepting, so the public URL must already resolve and answer
over TLS — then registers, receives the public key, saves it to
RELAY_JWT_PUBLIC_KEY, and starts enforcing auth immediately with no restart.
Later starts load the saved key and skip enrollment, so the code is needed
exactly once. From then on the controller health-checks the relay every minute.
Behind a TLS-inspecting firewall#
Enrollment is an outbound HTTPS call verified against the host’s certificate
store. On a network where a firewall re-signs TLS, it fails with
invalid peer certificate: UnknownIssuer — see
Troubleshooting.
Operating notes#
- A relay restart drops every parked agent. They reconnect within ~5 seconds, and in discovery mode may re-park somewhere else entirely.
- Nothing session-related is persisted. There is no session log to rotate and
no database to back up — only
control.pub.pem, and that is re-obtainable with a fresh enrollment code. - The controller marks a relay unhealthy when
/healthzstops answering. Agents then stop picking it, but agents already parked stay put until their connection drops.