fix(sidecar): refuse a plugin that names another server (D155) #15

Merged
whitlocktech merged 1 commits from fix/refuse-foreign-plugin into main 2026-09-26 06:27:17 +00:00
Member

Found by the phase 18 walk (§34.3 step 2). The org lead decided the sidecar should refuse and close rather than warn (D155). Merge together with Rust-Plugins#15: its second commit makes the plugin's backoff hold against a refusal, and without it a refused plugin redials about 250 times a second.

Why

[game].server_id was a cross-check that logged a WARN and kept the plugin's id. On the walk rig, a plugin bug (Rust-Plugins#15, D154) parked beta's plugin in alpha's listener backlog. The moment alpha's plugin reloaded, alpha's sidecar accepted beta, and the website showed server alpha with hostname "rg18 beta" and beta's wipe id. The only trace was nine WARN lines.

What changes

  • game.rs: with server_id set, the connection is closed on the first frame naming another server, before that frame reaches the store or the feed. Both ids are logged at ERROR, with the likely cause (two servers dialing one game port). A blank server_id checks nothing, as before.
  • The command channel opens only once a frame has named this server. Until then the peer could be a plugin about to be refused, and a website command sent in that window (a grant, a world write) would land on the wrong server. /health reads the same handle, so it reports plugin_connected only from that point too.
  • app.rs: the warn-only check_server_id is removed; game::serve takes the configured id.
  • egg/with-sidecar.sh: once the plugin's config exists, its ServerId is what the sidecar gets. The plugin reads RUSTLINK_SERVER_ID only when it writes its first config (D150). Without this, a panel variable edited after the first boot would take the bridge down instead of "changing nothing", which is what rust-link/INSTALL.md promises. The launcher prints one line saying which id it used and why. Tested in ghcr.io/pterodactyl/games:rust across four cases: first boot, an edited variable, a Carbon config, and an installer-shaped config.

No wire-message shape changes, so PROTOCOL_VERSION stays 12. PROTOCOL.md's rule "the plugin's id is the authority; the sidecar warns" is amended in the phase 18 docs PR.

Proof

  • cargo fmt, clippy -D warnings, and 66 tests including 3 new ones: the pure check, a foreign plugin refused over a real socket with nothing forwarded and no command channel, and the channel opening on a matching frame.
  • Rig (this branch's binary swapped into the phase 18 rig): with beta's plugin pointed at alpha's port, alpha's sidecar logged
    ERROR refusing a plugin that names another server: this sidecar is 'alpha', the plugin says 'beta'…, took alpha's own plugin back, and the site still showed alpha as rg18 alpha with alpha's wipe. With Rust-Plugins#15, refusals ran every 5 s. A restarted sidecar was rejoined by its own plugin in 0.49 s.

Merging releases v0.1.1 and triggers the installer's bundle.yml.

  • AI-assisted: Claude Code (Claude Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Found by the phase 18 walk (§34.3 step 2). The org lead decided the sidecar should **refuse and close** rather than warn (**D155**). **Merge together with Rust-Plugins#15**: its second commit makes the plugin's backoff hold against a refusal, and without it a refused plugin redials about 250 times a second. ## Why `[game].server_id` was a cross-check that logged a `WARN` and **kept the plugin's id**. On the walk rig, a plugin bug (Rust-Plugins#15, D154) parked beta's plugin in alpha's listener backlog. The moment alpha's plugin reloaded, alpha's sidecar accepted beta, and the website showed **server `alpha` with hostname "rg18 beta" and beta's wipe id**. The only trace was nine `WARN` lines. ## What changes - **`game.rs`**: with `server_id` set, the connection is closed on the first frame naming another server, **before** that frame reaches the store or the feed. Both ids are logged at `ERROR`, with the likely cause (two servers dialing one game port). A blank `server_id` checks nothing, as before. - **The command channel opens only once a frame has named this server.** Until then the peer could be a plugin about to be refused, and a website command sent in that window (a grant, a world write) would land on the wrong server. `/health` reads the same handle, so it reports `plugin_connected` only from that point too. - **`app.rs`**: the warn-only `check_server_id` is removed; `game::serve` takes the configured id. - **`egg/with-sidecar.sh`**: once the plugin's config exists, its `ServerId` is what the sidecar gets. The plugin reads `RUSTLINK_SERVER_ID` only when it writes its first config (D150). Without this, a panel variable edited after the first boot would take the bridge down instead of "changing nothing", which is what `rust-link/INSTALL.md` promises. The launcher prints one line saying which id it used and why. Tested in `ghcr.io/pterodactyl/games:rust` across four cases: first boot, an edited variable, a Carbon config, and an installer-shaped config. No wire-message shape changes, so `PROTOCOL_VERSION` stays 12. PROTOCOL.md's rule "the plugin's id is the authority; the sidecar warns" is amended in the phase 18 docs PR. ## Proof - `cargo fmt`, `clippy -D warnings`, and 66 tests including 3 new ones: the pure check, a foreign plugin refused over a real socket with nothing forwarded and no command channel, and the channel opening on a matching frame. - **Rig** (this branch's binary swapped into the phase 18 rig): with beta's plugin pointed at alpha's port, alpha's sidecar logged `ERROR refusing a plugin that names another server: this sidecar is 'alpha', the plugin says 'beta'…`, took alpha's own plugin back, and the site still showed alpha as **rg18 alpha** with alpha's wipe. With Rust-Plugins#15, refusals ran every 5 s. A restarted sidecar was rejoined by its own plugin in 0.49 s. Merging releases v0.1.1 and triggers the installer's `bundle.yml`. - [x] AI-assisted: Claude Code (Claude Opus 5.5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-09-26 06:20:09 +00:00
fix(sidecar): refuse a plugin that names another server (D155)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 4m1s
3f5ed059b8
`[game].server_id` was a cross-check that WARNED and kept the plugin's id.
The phase 18 walk showed what that costs: a second server's plugin,
parked in this listener's backlog by a plugin bug (Rust-Plugins, D154),
was accepted the moment the first server's plugin reloaded, and the
website showed server "alpha" with beta's hostname and wipe.

Now, with `server_id` set, the connection is closed on the first frame
that names another server, BEFORE that frame reaches the store or the
feed, and both ids are logged at ERROR. The command channel is installed
only once a frame has named this server, so no website command (a grant,
a world write) can reach a plugin about to be refused, and /health reports
the plugin connected only from then. Blank `server_id`: nothing checked,
as before.

The egg's launcher now hands the sidecar the plugin config's ServerId once
that file exists. The plugin reads RUSTLINK_SERVER_ID only at its first
config write (D150); without this, a variable edited after the first boot
would be refused instead of changing nothing, as INSTALL.md promises.

Walked: a plugin aimed at another server's sidecar is refused with an
ERROR naming both ids, and the site keeps the right server's identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit bc1919ce38 into main 2026-09-26 06:27:17 +00:00
whitlocktech deleted branch fix/refuse-foreign-plugin 2026-09-26 06:27:18 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Rust-Link#15
No description provided.