Files
docs/rust-link/INSTALL.md
wtclaude 1a7f52743f docs(rust): protocol 13 step 2 — expiry, plugin loads, the zone helper, the tally (§19.4-19.8, MODULE_API 1.11.0)
The spec for PLAN_FIXES §6 step 2, as built (D181-D185):

- PROTOCOL.md §19.4 world.expired's `what` and the website recording an
  expiry (amends §15's "maps it to nothing"); §19.5 plugin.loaded /
  plugin.unloaded with the permission diff; §19.6 the ZoneManager helper and
  `zoneHelper` at hello; §19.7 what the tally counts (F1, F3, F4); §19.8 the
  website (F7 hold, F5/F6 link fleet, F2 names).
- MODULE_API.md 1.11.0 and ctx.events.expired; EVENTS.md §L the `expired`
  status, terminal and green, and `resource.expired`.
- rust-link/INSTALL.md: the helper in the tarball, by hand, and in doctor.
- PLAYER_WALK.md: events step 6 can pass now; a step-2 section whose rows 1-2
  were walked on both rigs without a player and 3-9 need one.
- PLAN_FIXES §6: step 2 as built, with its PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 21:47:42 -05:00

14 KiB

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, and the design of record is ../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 — and beside it, from protocol 13, the optional ZoneManager helper RunicGatewayZones.cs (PLAN_FIXES D181, D182), installed by default Rust-Plugins
The sidecar rust-link-sidecar, which the plugin dials on loopback and the website reaches over HTTP Rust-Link

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 The server runs on a Linux host with systemd, or on Windows
B. The Pterodactyl egg The server runs on a Pterodactyl panel
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 — the same binary that sets up ServUO shards — with --game rust. Run it as root, or from an elevated PowerShell:

sudo ./runicgateway-installer-linux-x86_64 install --game rust --rust /srv/rust --server-id main
.\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 — the launcher hands the sidecar the id from the plugin's config and says so in the console
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 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/, and every other .cs the tarball's manifest.json lists in files beside it — from protocol 13 that is RunicGatewayZones.cs, which lets ZoneManager count a player already standing in a zone when it opens or comes back after a restart. It is optional: leave it out and events still score everybody, but ZoneManager's own flags miss that player. 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:

    [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; each helper deployed beside it, as a warning when missing or edited (the bridge runs without one, and the row says what that costs); 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
The sidecar logs refusing a plugin that names another server Two servers dial one sidecar: a plugin's Port is another server's game port. Correct that plugin's Port (in its config) and reload it. Nothing it sent was stored
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