fix(sidecar): refuse a plugin that names another server (D155)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 4m1s

`[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
This commit is contained in:
2026-09-26 01:19:50 -05:00
parent 1f3dfb55b2
commit 3f5ed059b8
4 changed files with 143 additions and 29 deletions

View File

@@ -31,6 +31,23 @@ for v in RUSTLINK_SERVER_ID RUSTLINK_WEB_TOKEN RUSTLINK_RETAIN_DAYS RUSTLINK_WEB
if [ -z "$val" ]; then unset "$v"; fi
done
# The server id follows the PLUGIN's config once it exists. The plugin reads RUSTLINK_SERVER_ID only
# when it writes its first config; after that the file is canonical and the website locks it (D150).
# The sidecar refuses a plugin that names another server (D155), so a variable edited after the
# first boot would otherwise take the bridge down instead of changing nothing. Stock framework paths,
# the same ones the installer writes. `sed`, because the game image has no jq.
for f in /home/container/oxide/config/RunicGateway.json /home/container/carbon/configs/RunicGateway.json; do
[ -f "$f" ] || continue
ID="$(sed -n 's/^[[:space:]]*"ServerId":[[:space:]]*"\([^"]*\)".*$/\1/p' "$f" | head -n 1)"
if [ -n "$ID" ]; then
if [ -n "${RUSTLINK_SERVER_ID-}" ] && [ "$RUSTLINK_SERVER_ID" != "$ID" ]; then
echo "[rust-link] RUSTLINK_SERVER_ID is '$RUSTLINK_SERVER_ID' but the plugin's config names '$ID'; using '$ID', the id the website knows. The variable is read once, at the first boot."
fi
export RUSTLINK_SERVER_ID="$ID"
fi
break
done
# The website-facing bind. Pterodactyl tells a container nothing about its extra allocations, so
# the port is typed into the egg, and it must be one of this server's allocations — a port that is
# not fails as a bind the website never reaches, which the lines below make visible (§34.1).