Adapter talks to whatever server is on the RCON port, so a wrong Zandronum path looks like success #1

Open
opened 2026-07-16 21:27:54 +00:00 by aiko · 2 comments
Owner

What happened

The pack appears to ignore its Launcher-sidecar paths. Point the zandronum path at a completely unrelated executable — a Nintendo DS emulator from another pack was used to prove this — and the game still comes up and effects still work, exactly as if the path were correct. Nothing reports a problem.

What's actually happening is worse than the path being ignored. The adapter spawns whatever binary the path names and then immediately connects over RCON to a fixed loopback port, without ever checking that the thing it spawned is the thing it's now talking to. If a Zandronum listen server is already running from an earlier, correct launch, the adapter connects straight to that leftover server. The bogus binary is spawned, fails or wanders off on its own, and is never noticed. Everything downstream looks healthy.

Confirmed on a live machine: with the DS emulator configured as the zandronum path, a Zandronum process from a previous session was still listening on the RCON port, and the adapter had been driving that server the whole time.

There is a second, related silence. When no zandronum path is configured at all, launching is skipped entirely and the adapter assumes a server is already up. That's a deliberate and reasonable manual/dev path, but combined with the above it means "no path configured", "garbage path configured", and "correct path configured" all converge on the same observable outcome whenever any server happens to be listening.

What I expected

A launcher path that cannot host the game should fail loudly, at launch, naming the path it tried. The adapter should not silently adopt a server it did not start. At minimum it should notice that the process it spawned died immediately, and it should be able to distinguish "the server I launched" from "a server that was already there" before treating the session as healthy.

Steps to reproduce

  1. Launch the pack once with a correct zandronum path and let the listen server come up.
  2. Stop the adapter, but leave the Zandronum server running (it survives).
  3. In the Launcher panel, change the zandronum path to any unrelated executable.
  4. Launch again. The bogus binary is spawned and goes nowhere; the adapter connects to the still-running server from step 1; the game and effects appear to work normally.
  5. Confirm the deception by checking which process holds the RCON port — it is the old server, not anything from this launch.

Additional context

The launch is fire-and-forget: the spawned process handle is kept only so it can be terminated at exit, and its exit status is never inspected. The RCON client is then pointed at the loopback host and port independently of what was spawned. The eager startup connect was deliberately added so that a wrong RCON password fails loudly at startup rather than on the first redeem — that instinct is right, and this issue is asking for the same treatment for a wrong binary.

Worth deciding while fixing: whether a pre-existing server should be adopted at all when a path is configured, or whether that should be an error telling the operator to stop the old server first. Note that a stale listen server holding the port is a known hazard in this project generally, so "detect and report" is likely more useful than "silently reuse".

When this is fixed, the tutorial must be updated in the same change. This repo's history is the tutorial: stages 1-9 are one commit each, and stage 2 is the one that introduces the RCON client and Launcher hosting, which is exactly the code at fault here. The README documents the Launcher flow and describes the no-path case as "assume a server is already up", so it needs to reflect whatever the new behavior becomes.

## What happened The pack appears to ignore its Launcher-sidecar paths. Point the `zandronum` path at a completely unrelated executable — a Nintendo DS emulator from another pack was used to prove this — and the game still comes up and effects still work, exactly as if the path were correct. Nothing reports a problem. What's actually happening is worse than the path being ignored. The adapter spawns whatever binary the path names and then immediately connects over RCON to a fixed loopback port, without ever checking that the thing it spawned is the thing it's now talking to. If a Zandronum listen server is already running from an earlier, correct launch, the adapter connects straight to that leftover server. The bogus binary is spawned, fails or wanders off on its own, and is never noticed. Everything downstream looks healthy. Confirmed on a live machine: with the DS emulator configured as the `zandronum` path, a Zandronum process from a previous session was still listening on the RCON port, and the adapter had been driving that server the whole time. There is a second, related silence. When no `zandronum` path is configured at all, launching is skipped entirely and the adapter assumes a server is already up. That's a deliberate and reasonable manual/dev path, but combined with the above it means "no path configured", "garbage path configured", and "correct path configured" all converge on the same observable outcome whenever any server happens to be listening. ## What I expected A launcher path that cannot host the game should fail loudly, at launch, naming the path it tried. The adapter should not silently adopt a server it did not start. At minimum it should notice that the process it spawned died immediately, and it should be able to distinguish "the server I launched" from "a server that was already there" before treating the session as healthy. ## Steps to reproduce 1. Launch the pack once with a correct `zandronum` path and let the listen server come up. 2. Stop the adapter, but leave the Zandronum server running (it survives). 3. In the Launcher panel, change the `zandronum` path to any unrelated executable. 4. Launch again. The bogus binary is spawned and goes nowhere; the adapter connects to the still-running server from step 1; the game and effects appear to work normally. 5. Confirm the deception by checking which process holds the RCON port — it is the old server, not anything from this launch. ## Additional context The launch is fire-and-forget: the spawned process handle is kept only so it can be terminated at exit, and its exit status is never inspected. The RCON client is then pointed at the loopback host and port independently of what was spawned. The eager startup connect was deliberately added so that a wrong RCON password fails loudly at startup rather than on the first redeem — that instinct is right, and this issue is asking for the same treatment for a wrong *binary*. Worth deciding while fixing: whether a pre-existing server should be adopted at all when a path *is* configured, or whether that should be an error telling the operator to stop the old server first. Note that a stale listen server holding the port is a known hazard in this project generally, so "detect and report" is likely more useful than "silently reuse". When this is fixed, the tutorial must be updated in the same change. This repo's history is the tutorial: stages 1-9 are one commit each, and stage 2 is the one that introduces the RCON client and Launcher hosting, which is exactly the code at fault here. The README documents the Launcher flow and describes the no-path case as "assume a server is already up", so it needs to reflect whatever the new behavior becomes.
Author
Owner

Findings from a code read — this issue's mechanism does not hold up

Parked pending a retest. Recording this so it is not re-derived, and so nobody builds from the current diagnosis.

The silence is real, but it is a different bug

main(), adapter.py:593:

# Connect eagerly so a wrong password fails loudly at startup instead of
# on the first redeem.
...
except RconError as e:
    if time.time() >= deadline:
        print(f"RCON not reachable yet ({e}); will retry per invocation",
              file=sys.stderr)
        break          # <-- adapter carries on

The comment is wrong. A failed eager connect is not fatal. After 8 seconds it prints one line to stderr and continues — then connects to MPP, sends hello, gets its ack, and registers as a healthy adapter. RCON being completely dead is a stderr line nobody reads.

This issue quotes that comment approvingly ("that instinct is right"). The instinct was never implemented. That is a genuine defect and is probably the real fix here:

  • make a failed eager connect fatal when we spawned the server (exit non-zero, naming the path we launched);
  • check _game_proc.poll() to notice the spawned binary dying immediately.

But the leftover-server story needs a precondition this issue never mentions

Password resolution, adapter.py:75:

if os.environ.get("DOOM_RCON_PASSWORD"):
    RCON_PASSWORD = os.environ["DOOM_RCON_PASSWORD"]   # stable across runs
elif ZANDRONUM_BIN:
    RCON_PASSWORD = secrets.token_hex(16)              # fresh per launch

The spawned server receives that same password (+sv_rconpassword, line 585). So the repro as written should fail: the leftover server from step 1 holds P1, the relaunch generates a fresh P2, and the connect gets SVRC_INVALIDPASSWORD (line 264) → RconError. Auth rejection is handled correctly and cannot look like success.

Adopting a leftover server therefore requires DOOM_RCON_PASSWORD to be set. Checked on the machine:

  • not set in Process, User, or Machine scope — no DOOM_* or *RCON* variables at all;
  • not in [adapter.env], and deliberately cannot be ("manifest values are hashed and visible");
  • nothing listening on UDP 10666.

So: the ephemeral password is already acting as a "did I start this server?" check. Accidentally, but effectively. DOOM_RCON_PASSWORD silently disables it — which is arguably the more interesting finding, and a smaller fix than this issue proposes.

What to confirm on retest

"The game still comes up and effects still work" is the claim that does not reconcile. DeSmuME spawned with Zandronum's argv cannot host RCON on 10666, so effects cannot work without a live Zandronum server and a matching password.

  • Was the window that appeared DOOM, or DeSmuME?
  • Did a redeem actually land in-game, or was it inferred from the adapter/Dashboard looking healthy?
  • Was DOOM_RCON_PASSWORD set in the shell that launched the bridge? (Transient — it is gone now.)

Machine state is preserved for the retest: mobrule-doom/.local/launcher.toml still points zandronum at the DeSmuME binary.

mobrule#176 settled on existence + executable validation only, and explicitly does not catch this case — DeSmuME is a real executable .exe and passes. Catching it is this issue's job.

## Findings from a code read — this issue's mechanism does not hold up Parked pending a retest. Recording this so it is not re-derived, and so nobody builds from the current diagnosis. ### The silence is real, but it is a different bug `main()`, `adapter.py:593`: ```python # Connect eagerly so a wrong password fails loudly at startup instead of # on the first redeem. ... except RconError as e: if time.time() >= deadline: print(f"RCON not reachable yet ({e}); will retry per invocation", file=sys.stderr) break # <-- adapter carries on ``` **The comment is wrong.** A failed eager connect is not fatal. After 8 seconds it prints one line to stderr and continues — then connects to MPP, sends `hello`, gets its ack, and registers as a healthy adapter. RCON being completely dead is a stderr line nobody reads. This issue quotes that comment approvingly ("that instinct is right"). The instinct was never implemented. That is a genuine defect and is probably the real fix here: - make a failed eager connect **fatal** when we spawned the server (exit non-zero, naming the path we launched); - check `_game_proc.poll()` to notice the spawned binary dying immediately. ### But the leftover-server story needs a precondition this issue never mentions Password resolution, `adapter.py:75`: ```python if os.environ.get("DOOM_RCON_PASSWORD"): RCON_PASSWORD = os.environ["DOOM_RCON_PASSWORD"] # stable across runs elif ZANDRONUM_BIN: RCON_PASSWORD = secrets.token_hex(16) # fresh per launch ``` The spawned server receives that same password (`+sv_rconpassword`, line 585). So the repro **as written should fail**: the leftover server from step 1 holds `P1`, the relaunch generates a fresh `P2`, and the connect gets `SVRC_INVALIDPASSWORD` (line 264) → `RconError`. Auth rejection is handled correctly and cannot look like success. Adopting a leftover server therefore requires `DOOM_RCON_PASSWORD` to be set. Checked on the machine: - **not set** in Process, User, or Machine scope — no `DOOM_*` or `*RCON*` variables at all; - **not** in `[adapter.env]`, and deliberately cannot be ("manifest values are hashed and visible"); - nothing listening on UDP 10666. So: **the ephemeral password is already acting as a "did I start this server?" check.** Accidentally, but effectively. `DOOM_RCON_PASSWORD` silently disables it — which is arguably the more interesting finding, and a smaller fix than this issue proposes. ### What to confirm on retest "The game still comes up and effects still work" is the claim that does not reconcile. DeSmuME spawned with Zandronum's argv cannot host RCON on 10666, so effects cannot work without a live Zandronum server **and** a matching password. - Was the window that appeared DOOM, or DeSmuME? - Did a redeem actually land in-game, or was it inferred from the adapter/Dashboard looking healthy? - Was `DOOM_RCON_PASSWORD` set in the shell that launched the bridge? (Transient — it is gone now.) Machine state is preserved for the retest: `mobrule-doom/.local/launcher.toml` still points `zandronum` at the DeSmuME binary. ### Related mobrule#176 settled on existence + executable validation only, and **explicitly does not catch this case** — DeSmuME is a real executable `.exe` and passes. Catching it is this issue's job.
Author
Owner

This was generated by AI during triage.

Triage Notes

Labelling this needs-info. The confirmed half of the diagnosis has been split into its own issue so it can proceed; what is left here is the part that a code read could not reconcile and that only a live retest can settle.

What we've established so far:

  • The silence this issue reports is real, but its mechanism is a different bug: the eager RCON connect is not fatal, so an adapter with completely dead RCON still registers as healthy. That is confirmed from the code and is now tracked separately, along with the missing exit-status check on the spawned process.
  • The leftover-server story requires a precondition this issue does not mention. The RCON password is either taken from DOOM_RCON_PASSWORD or, when a launcher path is configured, generated fresh per launch. The spawned server receives that same generated password. So the repro as written should fail: the leftover server from step 1 holds the old password, the relaunch mints a new one, and the connect is rejected with an invalid-password error, which is handled correctly and cannot look like success.
  • That makes the ephemeral password an accidental but effective "did I start this server?" check. DOOM_RCON_PASSWORD silently disables it, which is arguably the more interesting finding here.
  • Machine state at the time of the check: DOOM_RCON_PASSWORD was not set in Process, User, or Machine scope (no DOOM_* or *RCON* variables at all), it is not in [adapter.env] and deliberately cannot be, and nothing was listening on UDP 10666.
  • The claim that does not reconcile is "the game still comes up and effects still work". A DS emulator spawned with Zandronum's argv cannot host RCON on 10666, so effects should not be able to work without a live Zandronum server and a matching password.
  • Machine state is preserved for a retest: mobrule-doom/.local/launcher.toml still points zandronum at the DeSmuME binary.

What we still need from you (@aiko):

  1. Was the window that appeared actually DOOM, or was it DeSmuME?
  2. Did a redeem visibly land in-game, or was "effects still work" inferred from the adapter and Dashboard looking healthy?
  3. Was DOOM_RCON_PASSWORD set in the shell that launched the bridge at the time? This is the load-bearing question — without it set, the leftover-server adoption described here should be impossible. It is transient, so it cannot be recovered after the fact.

If the answer to 3 is "no" and the answer to 2 is "inferred", then this issue is fully explained by the split-out bug and this one can close. If a redeem genuinely landed, there is a real adoption path here that the password check should have prevented, and that is worth understanding properly.

> *This was generated by AI during triage.* ## Triage Notes Labelling this `needs-info`. The confirmed half of the diagnosis has been split into its own issue so it can proceed; what is left here is the part that a code read could not reconcile and that only a live retest can settle. **What we've established so far:** - The silence this issue reports is real, but its mechanism is a different bug: the eager RCON connect is not fatal, so an adapter with completely dead RCON still registers as healthy. That is confirmed from the code and is now tracked separately, along with the missing exit-status check on the spawned process. - The leftover-server story requires a precondition this issue does not mention. The RCON password is either taken from `DOOM_RCON_PASSWORD` or, when a launcher path is configured, generated fresh per launch. The spawned server receives that same generated password. So the repro as written should fail: the leftover server from step 1 holds the old password, the relaunch mints a new one, and the connect is rejected with an invalid-password error, which is handled correctly and cannot look like success. - That makes the ephemeral password an accidental but effective "did I start this server?" check. `DOOM_RCON_PASSWORD` silently disables it, which is arguably the more interesting finding here. - Machine state at the time of the check: `DOOM_RCON_PASSWORD` was not set in Process, User, or Machine scope (no `DOOM_*` or `*RCON*` variables at all), it is not in `[adapter.env]` and deliberately cannot be, and nothing was listening on UDP 10666. - The claim that does not reconcile is "the game still comes up and effects still work". A DS emulator spawned with Zandronum's argv cannot host RCON on 10666, so effects should not be able to work without a live Zandronum server *and* a matching password. - Machine state is preserved for a retest: `mobrule-doom/.local/launcher.toml` still points `zandronum` at the DeSmuME binary. **What we still need from you (@aiko):** 1. Was the window that appeared actually DOOM, or was it DeSmuME? 2. Did a redeem visibly land in-game, or was "effects still work" inferred from the adapter and Dashboard looking healthy? 3. Was `DOOM_RCON_PASSWORD` set in the shell that launched the bridge at the time? This is the load-bearing question — without it set, the leftover-server adoption described here should be impossible. It is transient, so it cannot be recovered after the fact. If the answer to 3 is "no" and the answer to 2 is "inferred", then this issue is fully explained by the split-out bug and this one can close. If a redeem genuinely landed, there is a real adoption path here that the password check should have prevented, and that is worth understanding properly.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mobrule/mobrule-pack-doom#1
No description provided.