From ab98af3d9802f0558e717267588741aeed82f82e Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:54:56 +0200 Subject: [PATCH 1/2] feat(rust): adopt the websocket RCON protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gameplane's agent now speaks Facepunch's WebRcon, so Rust no longer has to sit on consoleMode: pty with no RCON at all. Switches to rcon.protocol: websocket on 28016 with the image's RUST_RCON_PASSWORD. The image already enables webrcon by default (RUST_RCON_WEB=1) and serves plain ws:// (RUST_RCON_SECURE_WEBSOCKET=0), which is what the agent dials. This lights up the console, moderation, and — via playerlist — a real players list. playerlist returns a JSON array rather than lines, which is fine: the agent applies players.list.entryRegex with FindAll across the whole reply, so a DisplayName regex pulls out every player. lifecycle.stop now runs over RCON instead of pty-stdin. Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- rust/module.yaml | 2 +- rust/template.yaml | 52 +++++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/rust/module.yaml b/rust/module.yaml index 69ac8e3..5ae231c 100644 --- a/rust/module.yaml +++ b/rust/module.yaml @@ -1,7 +1,7 @@ apiVersion: gameplane.local/module/v1 name: rust displayName: Rust -version: 1.0.2 +version: 1.0.3 game: rust categories: [Survival, Shooter, PvP] summary: Rust dedicated server (didstopia/rust-server) with a pty console, Oxide plugin volume, and a save-then-quit stop sequence. diff --git a/rust/template.yaml b/rust/template.yaml index d216be5..2670139 100644 --- a/rust/template.yaml +++ b/rust/template.yaml @@ -1,17 +1,17 @@ # Gameplane GameTemplate for Rust (dedicated server). # -# Backed by didstopia/rust-server. Rust's RCON is WebSocket-only -# (Facepunch's own `rcon.web`/webrcon) — it does NOT speak the Source RCON -# wire protocol the agent implements, and there's no raw-TCP fallback. So -# rcon.protocol is "none" here; RCON-transport capabilities (players list, -# moderation, actions) are unavailable for Rust in Gameplane today. +# Backed by didstopia/rust-server. Rust's RCON is WebSocket-only — Facepunch's +# own `rcon.web`/webrcon, which does NOT speak the Source RCON wire protocol. +# The agent now implements it (rcon.protocol: websocket), so the console, +# players list, moderation, quiesce and quick actions all work over RCON. # -# RustDedicated does accept an attached interactive console (every -# Docker/tmux community wrapper drives it this way), which matches -# consoleMode: pty. The clean-stop sequence ("server.save" then "quit") is -# real and documented, and now actually runs: the operator's PTY-stdin stop -# transport writes it to the game container before scaling down, so a stop -# or restart saves the world instead of taking a SIGTERM mid-write. +# The image enables webrcon by default (RUST_RCON_WEB=1) and serves plain +# ws:// (RUST_RCON_SECURE_WEBSOCKET=0), which is what the agent's client +# dials. The password comes from RUST_RCON_PASSWORD, injected by the operator. +# +# playerlist returns a JSON array rather than lines, which is fine: the agent +# applies players.list.entryRegex with FindAll over the whole reply, so the +# DisplayName regex below pulls every player out of the single-line JSON. # # Cluster-scoped; apply once per cluster: # kubectl apply -f modules/rust/template.yaml @@ -42,11 +42,9 @@ spec: containerPort: 28015 protocol: UDP advertise: true - # Rust's RCON (28016) is WebSocket-over-TCP, not Source RCON — the - # agent can't dial it (see header comment). Declared here only so a - # player who wants to use their own WebRCON client can be routed to it - # via a networking.portOverrides / manual Service edit; Gameplane's own - # console/RCON features never use this port. + # Rust's RCON: WebSocket-over-TCP (webrcon), which the agent now speaks. + # Not advertised — the dashboard reaches it in-pod; exposing it publicly + # would put the RCON password on the internet. - name: rcon containerPort: 28016 protocol: TCP @@ -61,16 +59,28 @@ spec: limits: {cpu: "4", memory: 8Gi} rcon: - protocol: none + protocol: websocket + port: 28016 + # The image reads its RCON password from RUST_RCON_PASSWORD (default + # "docker" — the operator generates one and injects it here instead). + passwordEnv: RUST_RCON_PASSWORD - consoleMode: pty + consoleMode: rcon capabilities: - # Runs over the pty-stdin transport once the operator's PTY-stop path - # lands (see header comment) — both commands are Facepunch/community- - # documented as the clean-shutdown sequence. + # Facepunch/community-documented clean-shutdown sequence. Now runs over + # RCON rather than pty-stdin, so a stop or restart saves the world instead + # of taking a SIGTERM mid-write. lifecycle: stop: ["server.save", "quit"] + players: + # playerlist returns a JSON array of player objects; entryRegex is applied + # with FindAll across the whole reply, so this lifts out every DisplayName. + list: + command: "playerlist" + entryRegex: '"DisplayName":"(?P[^"]*)"' + kick: 'kick "{{.Player}}"{{if .Reason}} "{{.Reason}}"{{end}}' + ban: 'ban "{{.Player}}"{{if .Reason}} "{{.Reason}}"{{end}}' mods: loaders: oxide: From adb825260976a572a11c421d547335a413966ce1 Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:57:47 +0200 Subject: [PATCH 2/2] chore(validate): allow rcon.protocol websocket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The validator's protocol allowlist is what stops a module shipping a console that can never connect, so it correctly rejected 'websocket' until the agent actually implemented it. It does now (Facepunch WebRcon, ValgulNecron/Gameplane#141), so add it — and lift the list into a named constant rather than repeating it in the message, so the two can't drift. Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- validate.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/validate.py b/validate.py index cd8d792..3ba1e88 100644 --- a/validate.py +++ b/validate.py @@ -120,6 +120,13 @@ CURL_TIMEOUT_SECS = 15 +# Wire protocols the agent's rcon package actually implements. Keep in sync with +# the GameTemplate CRD's rcon.protocol enum and agent/internal/rcon/. A protocol +# listed here but not implemented lets a module ship a console that never +# connects, so this list is deliberately conservative. +RCON_PROTOCOLS = ("source", "telnet", "websocket", "none") + + @dataclass class Finding: @@ -627,13 +634,13 @@ def rule_rcon_protocol(spec: dict) -> list[Finding]: # The CRD marks Protocol +kubebuilder:default=source, so an omitted # key resolves to "source" at apply time — not a missing/invalid value. return [] - if proto not in ("source", "telnet", "none"): + if proto not in RCON_PROTOCOLS: return [ Finding( ERROR, "bad-rcon-protocol", - f"rcon.protocol={proto!r} is not one of source/telnet/none — " - "the agent implements no others.", + f"rcon.protocol={proto!r} is not one of " + f"{'/'.join(sorted(RCON_PROTOCOLS))} — the agent implements no others.", ) ] return []