docs(rust): the Rust operator guide and bundle schema 2 (phase 18, draft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -1309,6 +1309,36 @@ version anywhere.** It reads what the artifacts declare, and §7.1's gate 1 is w
|
||||
mismatched pair from being published as a bundle — which is the mechanism that will matter at the
|
||||
*next* protocol bump, not just this one. See `docs/link/v3.md`.
|
||||
|
||||
### 7.5 Schema 2: a second game (module-rust phase 18)
|
||||
|
||||
Rust reached the installer in module-rust phase 18 (`docs/modules/rust/PLAN.md` §34, D146–D148), and
|
||||
schema 1 could not carry it: it names ServUO in its *shape* (`link`, `overlay.servuo`), and every
|
||||
shipped installer refuses any schema but 1. So the bundle gained a second schema, **for both games**,
|
||||
rather than a separate format for Rust:
|
||||
|
||||
- **Schema 2 names one game.** A `game` discriminant (`servuo` or `rust`), a `sidecar`, and a
|
||||
`payload` whose `kind` is `overlay` or `plugin`. One document per game, because the games release
|
||||
on their own schedules and a document naming both would hand a ServUO host a new bundle every time
|
||||
a Rust plugin shipped. The shape is in `installer/bundles/README.md`.
|
||||
- **Three streams on the `bundles` branch:** schema 1 at the root (ServUO), `v2/servuo/`, and
|
||||
`v2/rust/`. `bundle.yml` composes all three in `.gitea/scripts/compose-bundles.sh`, each game
|
||||
independently — a Rust failure still lets a ServUO bundle publish, and the run goes red after. A
|
||||
game none of whose repos has released composes nothing, and that is not a failure.
|
||||
- **One matrix, one tag.** A ServUO pair carries the same tag at both schemas, so the first
|
||||
`v2/servuo` bundle is `2026.09.15` — the tag schema 1 had already given that pair.
|
||||
- **Schema 1 is kept alive until 2027-01-01** (D147), composed exactly as before so every installer
|
||||
in the field keeps updating. After that date it is **frozen, never deleted**: an old installer still
|
||||
resolves its last bundle and every `bundle-<tag>.json` stays pinnable.
|
||||
- **The installer from phase 18 reads both.** ServUO resolves `v2/servuo/` first and is *lowered*
|
||||
into the schema-1 model, so `install.json` and every ServUO code path are unchanged; a pin that
|
||||
exists only at schema 1 falls back to the root. Rust resolves `v2/rust/`.
|
||||
- **v2 is read through Gitea's contents API**, not `/raw/`: the web raw route answers
|
||||
`Cache-Control: public, max-age=21600`, so a bundle read through it can be hours behind the
|
||||
branch. The contents API is `private, must-revalidate`. (Schema 1's fallback still uses `/raw/`,
|
||||
as every shipped installer does.)
|
||||
|
||||
What `--game rust` installs from a Rust bundle, and where, is `docs/rust-link/INSTALL.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open questions
|
||||
|
||||
234
rust-link/INSTALL.md
Normal file
234
rust-link/INSTALL.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# Installing the Rust bridge
|
||||
|
||||
This connects **Rust servers** — running Oxide or Carbon — to a Runic Gateway website that has the
|
||||
Rust module. It is the Rust counterpart of [`../installer/INSTALL.md`](../installer/INSTALL.md), and
|
||||
the design of record is [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §34.
|
||||
|
||||
## What this installs
|
||||
|
||||
Two components per Rust server, released together as a **bundle** — an exact pair CI has checked
|
||||
speaks one protocol, never "the latest of each":
|
||||
|
||||
| Component | What it is | Released from |
|
||||
|---|---|---|
|
||||
| **The plugin** | `RunicGateway.cs`, one file that runs unchanged on Oxide and Carbon | [Rust-Plugins](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/releases) |
|
||||
| **The sidecar** | `rust-link-sidecar`, which the plugin dials on loopback and the website reaches over HTTP | [Rust-Link](https://gitea.whitlocktech.com/RunicGateway/Rust-Link/releases) |
|
||||
|
||||
The game server opens no port for the bridge: the plugin is the client and the sidecar the
|
||||
listener, on `127.0.0.1`. **One sidecar serves one Rust server.** A community running six servers
|
||||
runs six sidecars, each with its own port, database and token, and the website holds six entries.
|
||||
|
||||
There are three ways to set a server up. They produce the same result:
|
||||
|
||||
| | Use it when |
|
||||
|---|---|
|
||||
| [**A. The installer**](#a-the-installer) | The server runs on a Linux host with systemd, or on Windows |
|
||||
| [**B. The Pterodactyl egg**](#b-the-pterodactyl-egg) | The server runs on a Pterodactyl panel |
|
||||
| [**C. By hand**](#c-by-hand) | Neither fits, or you want to place every file yourself |
|
||||
|
||||
## Before you begin
|
||||
|
||||
- **Oxide or Carbon is installed, and the server has started once** with it, so its directories
|
||||
exist. The bridge is a plugin; a vanilla server has nothing to load it.
|
||||
- **Kits and ZoneManager** from uMod are what the reward and zone features use. The bridge works
|
||||
without them and names what is missing; nothing here installs them.
|
||||
- **The website has the Rust module**, and you are an administrator on it. Servers are added at
|
||||
**Admin → Rust → Servers** (`/admin/rust/servers`).
|
||||
|
||||
> **Until module-rust phase 19**, the released Rust module (Module-Rust v0.3.0) speaks protocol 2
|
||||
> while the released bridge speaks 12, and a site pairing them answers `409`. This is expected and
|
||||
> closes with phase 19's release; it is not a fault in your install.
|
||||
|
||||
---
|
||||
|
||||
## A. The installer
|
||||
|
||||
The [Runic Gateway installer](https://gitea.whitlocktech.com/RunicGateway/installer/releases) —
|
||||
the same binary that sets up ServUO shards — with `--game rust`. Run it as root, or from an
|
||||
elevated PowerShell:
|
||||
|
||||
```bash
|
||||
sudo ./runicgateway-installer-linux-x86_64 install --game rust --rust /srv/rust --server-id main
|
||||
```
|
||||
|
||||
```powershell
|
||||
.\runicgateway-installer-windows-x86_64.exe install --game rust --rust D:\rust --server-id main
|
||||
```
|
||||
|
||||
| Flag | |
|
||||
|---|---|
|
||||
| `--rust <path>` | The server root: the directory holding `RustDedicated` |
|
||||
| `--server-id <id>` | **Required.** The id the website will know this server by: lowercase letters, digits and `-`, up to 64. It names this server's sidecar, service and files |
|
||||
| `--web-port <port>` | The port the website reaches this sidecar on. Default: the first free from 8090 |
|
||||
| `--bundle <tag>` | Install an exact published bundle instead of the current one |
|
||||
| `--verify` | Report everything it would do, and write nothing |
|
||||
| `--host <name>` | The hostname to print in the sidecar URL |
|
||||
| `--site-url <url>` | Your site, so the printed link opens the right page |
|
||||
|
||||
What a run does:
|
||||
|
||||
1. **Resolves the bundle** for Rust and checks the plugin's own manifest against it.
|
||||
2. **Detects the framework** — Oxide or Carbon — from the server itself. A tree with both is
|
||||
refused (they cannot run together); one with neither is refused with what to install.
|
||||
3. **Writes the plugin's config** (`oxide/config/RunicGateway.json` or
|
||||
`carbon/configs/RunicGateway.json`) holding just `ServerId` and `Port`, **only if there is none**.
|
||||
The plugin fills in every other key on its first load. An existing config is never rewritten,
|
||||
and one that names a different server stops the run and says so — the website locks a server's
|
||||
id once it has seen it.
|
||||
4. **Installs the sidecar**, writes its config with this server's ports, and registers its service.
|
||||
5. **Places the plugin** in `oxide/plugins/` or `carbon/plugins/`. Both frameworks load a plugin
|
||||
the moment it lands, so on a running server the bridge comes up at once; on a stopped one, at
|
||||
the next boot. The installer never starts or stops your server.
|
||||
6. **Prints what the website needs.**
|
||||
|
||||
### More than one server on a host
|
||||
|
||||
Run it once per server, each with its own `--rust` and `--server-id`. Each gets its own service,
|
||||
config, database, game port (from 7799) and web port (from 8090). They share one sidecar binary, so
|
||||
`update` moves them all together.
|
||||
|
||||
### Where everything lands
|
||||
|
||||
| | Linux | Windows |
|
||||
|---|---|---|
|
||||
| The sidecar binary (shared) | `/usr/bin/runicgateway-rust-link` | `%ProgramFiles%\RunicGateway\rust-link-sidecar.exe` |
|
||||
| One server's sidecar config — **holds its token** | `/etc/runicgateway/rust/<id>.toml` (mode 600) | `%ProgramData%\RunicGateway\rust\<id>\sidecar.toml` |
|
||||
| One server's database | `/var/lib/runicgateway/rust/<id>/rust-link.db` | beside its config |
|
||||
| One server's service | `runicgateway-rust@<id>.service` | `RunicGatewayRust-<id>`, as `NT SERVICE\RunicGatewayRust-<id>` |
|
||||
| The record | `/etc/runicgateway/rust/install.json` | `%ProgramData%\RunicGateway\rust\install.json` |
|
||||
|
||||
The Rust record is separate from ServUO's, so a host running both games keeps two records that
|
||||
cannot interfere. The token is in the sidecar config and nowhere else — not in the record.
|
||||
|
||||
---
|
||||
|
||||
## B. The Pterodactyl egg
|
||||
|
||||
Each Rust-Link release carries **`egg-rust-runicgateway.json`**: egg 18 "Rust Autowipe" with the
|
||||
bridge added. A panel administrator imports it (**Admin → Nests → Import Egg**); the panel has no
|
||||
API for writing an egg.
|
||||
|
||||
When you create a server from it:
|
||||
|
||||
| Variable | |
|
||||
|---|---|
|
||||
| **Modding Framework** (`FRAMEWORK`) | `oxide` or `carbon`. `vanilla` installs no bridge and the server boots exactly as egg 18's |
|
||||
| **Runic Gateway: server id** (`RUSTLINK_SERVER_ID`) | The id on the website. Read **once**, when the plugin writes its first config; after that the file holds it and the site locks it, so changing the variable later changes nothing |
|
||||
| **Runic Gateway: sidecar port** (`RUSTLINK_WEB_PORT`) | **One of this server's allocations.** The panel does not tell a server which ports it holds, so a port that is not allocated binds but is never reachable |
|
||||
| **Runic Gateway: sidecar token** (`RUSTLINK_WEB_TOKEN`) | Leave blank: the sidecar generates one. Anyone who can see this server's startup variables can read a token typed here |
|
||||
| **Runic Gateway: bundle** (`RUNICGATEWAY_BUNDLE`) | Blank takes the current bundle. A tag pins one |
|
||||
| **Runic Gateway: history days** (`RUSTLINK_RETAIN_DAYS`) | Blank keeps the default |
|
||||
|
||||
**The install** downloads the sidecar, its launcher and the plugin from the bundle, checks each
|
||||
against the bundle's checksum, and only then places them: the sidecar in `rust-link/`, the plugin in
|
||||
`oxide/plugins/` or `carbon/plugins/`. Any mismatch fails the install with the reason, before
|
||||
anything is placed.
|
||||
|
||||
**The first boot** prints, in the console, the token the sidecar generated — **once** — and a line
|
||||
with what the website needs:
|
||||
|
||||
```
|
||||
[rust-link] A new sidecar token was generated. It is shown ONCE, here:
|
||||
[rust-link] 4f9c…
|
||||
[rust-link] It is kept in rust-link/sidecar.toml; read it there if you lose it.
|
||||
[rust-link] Admin -> Rust -> Servers: server id 'main', sidecar URL http://<this node's address>:21009 (listening on 0.0.0.0:21009)
|
||||
```
|
||||
|
||||
Every later boot prints only the second line. The sidecar runs inside the server's container
|
||||
and stops when the server stops.
|
||||
|
||||
**Updating is a reinstall.** The bridge is fetched at install and reinstall only, never on a
|
||||
restart, so a restart can never change the protocol under a website that has not been updated.
|
||||
Reinstall with **Regen Server** off to take the current bundle (or the pinned one) and keep the map.
|
||||
|
||||
**A wipe keeps the bridge's history.** With Regen Server on, the install script deletes
|
||||
`REMOVE_FILES` — and moves `rust-link/` out of the way while it does, so no list, wildcards
|
||||
included, can reach the database or the token. After a wipe the website still has the server's
|
||||
history, and the token is unchanged.
|
||||
|
||||
---
|
||||
|
||||
## C. By hand
|
||||
|
||||
1. **Pick the bundle** — the current one is
|
||||
[`v2/rust/current.json`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/bundles/v2/rust/current.json)
|
||||
on the installer's `bundles` branch. It names the sidecar binary for your platform and the
|
||||
plugin tarball, each with a `sha256`.
|
||||
2. **Download and verify** both against those checksums (`sha256sum -c`, or `Get-FileHash`).
|
||||
3. **The plugin:** copy `runicgateway-rust-plugin/RunicGateway.cs` from the tarball into
|
||||
`oxide/plugins/` or `carbon/plugins/`. To name the server, create its config first —
|
||||
`oxide/config/RunicGateway.json` or `carbon/configs/RunicGateway.json` — holding
|
||||
`{ "ServerId": "<id>", "Port": 7799 }`. Without it, the plugin calls the server `main`.
|
||||
4. **The sidecar:** put the binary somewhere stable and give it a config:
|
||||
|
||||
```toml
|
||||
[game]
|
||||
bind = '127.0.0.1:7799' # the plugin's Port
|
||||
server_id = '<id>'
|
||||
|
||||
[web]
|
||||
bind = '127.0.0.1:8090'
|
||||
auth_token = "" # generated and saved on first start
|
||||
|
||||
[store]
|
||||
path = '/var/lib/rust-link/<id>/rust-link.db'
|
||||
```
|
||||
|
||||
`rust-link-sidecar --print-config --config <file>` generates the token, saves it, and prints it.
|
||||
5. **Run it** under your supervisor of choice with `--config <file>`, as an unprivileged user that
|
||||
can write the database directory. On Windows the same `.exe` runs as a service
|
||||
(`sc.exe create … binPath= "\"<exe>\" --config \"<file>\""`); it speaks the service handshake
|
||||
itself.
|
||||
|
||||
Every environment variable the sidecar reads (`RUSTLINK_CONFIG`, `RUSTLINK_WEB_BIND`,
|
||||
`RUSTLINK_WEB_TOKEN`, …) overrides the file; an empty one is ignored.
|
||||
|
||||
---
|
||||
|
||||
## Connect the website
|
||||
|
||||
On your site, open **Admin → Rust → Servers** and add the server:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Id | the server id — the installer's `--server-id`, or the egg's variable |
|
||||
| Name | anything; players see it |
|
||||
| Sidecar address | the sidecar URL the installer or the console printed |
|
||||
| Sidecar token | the token |
|
||||
|
||||
Test it from the same page: it should answer *plugin connected* with the protocol.
|
||||
|
||||
### If the website is on another machine
|
||||
|
||||
The installer's sidecar listens on `127.0.0.1`, reachable only from the game host. Put a TLS
|
||||
reverse proxy (or a VPN link) in front of it and give the website the proxy's URL — the token is a
|
||||
plain bearer token, and the sidecar speaks HTTP. The egg's sidecar listens on its allocation,
|
||||
which is reachable from the network; firewall it to the website's address. **Leave the game bind
|
||||
on loopback either way**: that socket takes commands for the game server and has no token, and
|
||||
being loopback-only is what makes that safe.
|
||||
|
||||
---
|
||||
|
||||
## Day two
|
||||
|
||||
With the installer:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `doctor --game rust [--server-id <id>]` | Per server: the framework; whether the plugin file is still the one deployed; that the plugin's config names this server; the required uMod plugins; the service; and `/health` through to **plugin connected**. A stopped server is a warning; a running one whose plugin never connected is a failure, printed with the framework versions the plugin is known good on |
|
||||
| `update --game rust` | Moves the sidecar and every server's plugin to the current bundle, and restarts the sidecars. Always all servers together — they share one binary |
|
||||
| `uninstall --game rust [--server-id <id>] [--purge]` | Removes the service and the plugin file. **Keeps the plugin's config** — it is the website's, and it names the server. `--purge` also removes the sidecar config (the token) and the database. Removing the last server removes the shared binary too |
|
||||
|
||||
With the egg: reinstall to update (above); the console is the diagnosis.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause |
|
||||
|---|---|
|
||||
| The install stops: *already names this server "x"* | The plugin's config names another server. Use that id, or — if the server really is new to the site — remove the config file |
|
||||
| The install stops: *has both Oxide and Carbon* | A tree mid-migration. Remove the framework you are leaving |
|
||||
| The site says the sidecar is unreachable | The website cannot reach the URL: a loopback bind with the site elsewhere (see above), a firewall, or — on the egg — a sidecar port that is not one of the server's allocations |
|
||||
| The site answers `409` | The site's Rust module speaks another protocol than the bridge. Update whichever is behind (and see the note in *Before you begin*) |
|
||||
| `doctor`: *plugin connected — no, and the server is running* | The plugin did not compile or cannot reach its sidecar. The server console names a compile error; the plugin's `Port` must equal the sidecar's game port |
|
||||
| `doctor`: *not the file that was deployed* | The plugin was edited by hand. `update --game rust` puts the released one back |
|
||||
| The egg's console prints a new token after every boot | Only a sidecar older than the first Rust-Link release does this; reinstall the server |
|
||||
Reference in New Issue
Block a user