The rust-link sidecar: it owns the loopback listener the Oxide bridge plugin dials into, and serves the website a WebSocket feed plus store-backed reads. Protocol 1 is deliberately three frames — server.hello, ping/pong, and one correlated server.status — because phase 1's job is to get every seam working at once with almost nothing in them. What is load-bearing rather than incidental: * The plugin is the TCP client and this process owns the listener, so a Rust server opens no extra port. Loopback is the trust boundary on that link and there is no token on it; the website-facing surface is the opposite, with auth always on and a token generated and persisted on first start. * Inbound lines are capped at 1 MiB from the start rather than after the first large frame arrives. An over-long line is discarded and the connection stays up: one malformed frame is not a reason to drop a link live events flow over. * Store-backed reads answer while the game is off, which is what lets a website render a server list during a wipe. /status is the one route that fails when the game is down, and /server answers 204 rather than a null when the game has never connected -- those are different answers and a client that cannot tell them apart renders a server that does not exist. * The two RPC failures get distinct codes. 503 means the game is down; 504 means it is up and did not answer. Different fixes. * rpc::REPLY_TIMEOUT is a ceiling every later command budget sits under: core classifies a budget overrun as retryable unconditionally, so an action whose budgetMs does not exceed it can never report retry:false. One defect found while building, which no unit test would have caught: a four-connection SQLite pool over :memory: hands out four separate empty databases, because an in-memory database is per connection. It presents as 'no such table' from a random subset of queries. The pool is now capped at one connection for an in-memory path, which is the only coherent reading of :memory: and is what makes it usable at all. Exercised end to end against a live Rust server: a server.hello travelled game -> sidecar -> module -> the public website API, and killing this process left the game untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
72 lines
3.2 KiB
Markdown
72 lines
3.2 KiB
Markdown
# rust-link
|
|
|
|
The **sidecar** half of the Runic Gateway bridge for [Rust](https://rust.facepunch.com/). It
|
|
terminates the loopback link from a Rust server's Oxide bridge plugin and exposes the WebSocket +
|
|
REST surface the website consumes.
|
|
|
|
It is the mirror of [`RunicGateway/link`](https://gitea.whitlocktech.com/RunicGateway/link), which
|
|
does the same job for Ultima Online, and it keeps that bridge's central invariant unchanged:
|
|
|
|
> **The game server is never reachable from the website.** The plugin dials *out* to this process;
|
|
> this process owns the listener. Only the sidecar is exposed, and only the website's backend talks
|
|
> to it.
|
|
|
|
```
|
|
Rust server + Oxide (RunicGateway/Rust-Plugins, C#)
|
|
│ loopback TCP 127.0.0.1:7799, newline-delimited JSON, bidirectional
|
|
│ the PLUGIN dials out (the game opens no listening port for us)
|
|
▼
|
|
rust-link sidecar (this repo, Rust) ← the only network-facing bridge component
|
|
│ WebSocket (live feed) + REST (point-in-time reads), bearer-token auth
|
|
▼
|
|
website backend + module-rust (RunicGateway/Module-Rust, Node)
|
|
```
|
|
|
|
## One server, one sidecar
|
|
|
|
This binary serves **exactly one** Rust game server. A community running six servers runs six
|
|
pairs, each with its own port, database and token; `module-rust` holds six clients and the website
|
|
core never learns there is more than one. Nothing here is multiplexed, and nothing here should
|
|
become multiplexed.
|
|
|
|
## Build and run
|
|
|
|
```bash
|
|
cd sidecar
|
|
cargo build --release # → target/release/rust-link-sidecar
|
|
cargo run # info logging; writes sidecar.toml (with a generated token) on first run
|
|
RUST_LOG=debug cargo run # verbose, including heartbeats
|
|
```
|
|
|
|
Everything is configured from `sidecar.toml` — nothing is compiled into the binary. **Auth is
|
|
always on**: a blank token is generated and written back on first start, so there is no state in
|
|
which this process listens without one. `--print-config` resolves the configuration and prints it
|
|
as JSON, which is how an installer reads the token back without scraping a log.
|
|
|
|
See [`sidecar/README.md`](sidecar/README.md) for the configuration reference and the endpoint list.
|
|
|
|
## The protocol is a contract
|
|
|
|
The loopback JSON protocol (plugin ↔ sidecar) and this sidecar's HTTP/WS API (sidecar ↔ website)
|
|
are **versioned compatibility contracts**, not build dependencies. `PROTOCOL_VERSION` lives in
|
|
[`sidecar/src/main.rs`](sidecar/src/main.rs); every response carries `X-RustLink-Version`, and a
|
|
client that declares a different one is refused `409` rather than served something it will
|
|
mis-parse.
|
|
|
|
A version is declared in **three** places and they must agree:
|
|
|
|
| Where | Repo |
|
|
|---|---|
|
|
| `PROTOCOL_VERSION` | this repo |
|
|
| `overlay.toml` | [`RunicGateway/Rust-Plugins`](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins) |
|
|
| `module.json` | [`RunicGateway/Module-Rust`](https://gitea.whitlocktech.com/RunicGateway/Module-Rust) |
|
|
|
|
The canonical spec is
|
|
[`docs/rust-link/`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/rust-link). If
|
|
you add or change an event or a command, update all three repos **and** the spec in the same
|
|
change.
|
|
|
|
## Licence
|
|
|
|
GPL-3.0-or-later. See [LICENSE.md](LICENSE.md).
|