docs: the browser client connects DIRECTLY to netmux — correct #2215's README

The README merged in #2216 claimed browsers cannot reach netmux and
that both transports require Hydra.  Wrong on the central point:
netmux serves WebSocket natively on its ordinary game ports
(mux/src/websocket.cpp, RFC 6455) with first-byte protocol detection
sharing each port between telnet and WebSocket (#1074/#2193,
proto_detect_window), and the handshake accepts both /wsclient and /
— which is exactly the path js/connection.js dials.  The minimal
browser deployment is netmux plus static files, no extra process;
Hydra is the OPTIONAL layer for session resume, multi-game links,
stored credentials, and gRPC-Web.

The error came from concluding absence out of a truncated grep: the
file listing was piped through head and mux/proxy's matches filled the
window before mux/src/websocket.cpp appeared.  A truncated listing is
not a complete listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stephen Dennis 2026-08-07 22:12:06 -06:00
parent 02c4a6acca
commit 4e94bb75aa

View file

@ -9,45 +9,60 @@ browser's localStorage.
## How a browser reaches a game
Browsers cannot open raw TCP, so the client never talks to `netmux`
directly. Both of its transports go through **Hydra**, the connection
proxy in [`mux/proxy`](../../mux/proxy), deployed alongside the game:
Browsers cannot open raw TCP, but they do not need a middlebox either:
**netmux serves WebSocket natively on its ordinary game ports**
(`mux/src/websocket.cpp`, RFC 6455). A port is shared between telnet
and WebSocket by first-byte protocol detection: a WebSocket client
sends its HTTP `GET` immediately and is upgraded (`/wsclient` and `/`
are both accepted); a classic telnet client waits for the server to
speak and receives the banner after the `proto_detect_window` grace
period (#1074/#2193 — runtime-settable, `0` disables detection for a
telnet-only port). This works on plain and TLS ports alike, so `wss:`
direct to the game is a two-line config away.
```
browser ── ws(s) / gRPC-Web ──> hydra ── telnet TCP ──> netmux
browser ── ws(s) ─────────────────> netmux game port (shared with telnet)
browser ── ws(s) / gRPC-Web ──> hydra ── telnet ──> netmux
```
- **Telnet over WebSocket** (`js/connection.js`) — the simple path.
Point the client at a Hydra `websocket` / `websocket+tls` listener and
bytes flow as-is; the client's own telnet engine (`js/telnet.js`)
handles negotiation, `IAC GA` prompts, NAWS, TTYPE, and CHARSET.
Any other telnet-over-WebSocket bridge works here too; nothing in
this path is Hydra-specific.
- **Telnet over WebSocket** (`js/connection.js`) — the simple path,
and the direct one. Point the client at the game port (or at a
Hydra `websocket` listener, or any generic telnet-over-WebSocket
bridge — nothing in this path cares which) and bytes flow as-is; the
client's own telnet engine (`js/telnet.js`) handles negotiation,
`IAC GA` prompts, NAWS, TTYPE, and CHARSET.
- **Hydra GameSession** (`js/hydra_connection.js`) — the full protocol:
protobuf `ClientMessage`/`ServerMessage` over a WebSocket with the
`hydra-gamesession` subprotocol, falling back to gRPC-Web
unary/streaming where WebSocket is blocked. This is what the
`/hconnect`, `/hswitch`, `/hlinks`, `/hgames`, `/hscroll` commands
drive (`/hhelp` lists them): multiple game links in one session,
server-side scrollback fetch, and Hydra-stored credentials with
auto-login. Session resume means a dropped browser connection picks
up where it left off — the proxy holds the game link open.
- **Hydra GameSession** (`js/hydra_connection.js`) — the optional
richer protocol, via **Hydra**, the connection proxy in
[`mux/proxy`](../../mux/proxy): protobuf `ClientMessage`/
`ServerMessage` over a WebSocket with the `hydra-gamesession`
subprotocol, falling back to gRPC-Web unary/streaming where
WebSocket is blocked. This is what the `/hconnect`, `/hswitch`,
`/hlinks`, `/hgames`, `/hscroll` commands drive (`/hhelp` lists
them): multiple game links in one session, server-side scrollback
fetch, and Hydra-stored credentials with auto-login. Session resume
means a dropped browser connection picks up where it left off — the
proxy holds the game link open across it, which the direct path
cannot do.
## Deploying
1. Build and run Hydra next to the game: `cd mux/proxy && make`, copy
`hydra.conf.example` to `hydra.conf`, and enable the listeners you
want. See that file for TLS certificates, the master key that
encrypts stored credentials, and health-check endpoints.
2. Serve this directory from any static host — nginx, a CDN, an S3
The minimal deployment is netmux plus static files — no extra process:
1. Serve this directory from any static host — nginx, a CDN, an S3
bucket. There is nothing to compile.
3. Two cross-origin rules to respect:
- A page served over `https` may only open `wss:`/`https:`
transports (mixed-content rule), so front the Hydra listeners with
TLS in any real deployment.
- gRPC-Web listeners deny cross-origin requests by default; add your
client's origin with `cors_origin` in `hydra.conf`.
2. Point the client at the game's host and port. A page served over
`https` may only open `wss:` (mixed-content rule), so use a TLS
game port for any real deployment.
Add Hydra when you want what the proxy provides — session resume,
multi-game links, stored credentials, gRPC-Web for WebSocket-hostile
networks: `cd mux/proxy && make`, copy `hydra.conf.example` to
`hydra.conf`, and enable the listeners you want. See that file for
TLS certificates, the master key that encrypts stored credentials,
health-check endpoints, and `cors_origin` (gRPC-Web listeners deny
cross-origin requests by default; plain WebSocket is not subject to
CORS).
## Development
@ -66,6 +81,7 @@ it stays testable this way.
## Status
Part of the 2.14 development tree. The released 2.13 server has no
Hydra, so a 2.13 game wanting browser play needs a generic
telnet-over-WebSocket bridge and the simple transport above.
Part of the 2.14 development tree. The released 2.13 server has
neither the WebSocket listener nor Hydra, so a 2.13 game wanting
browser play needs a generic telnet-over-WebSocket bridge and the
simple transport above.