Skip to content

fix(rcon): send replies, buffer fragmented packets, harden auth - #201

Merged
Rushaway merged 3 commits into
masterfrom
fix/185-rcon-replies-framing
Oct 14, 2026
Merged

Rushaway merged 3 commits into
masterfrom
fix/185-rcon-replies-framing

Conversation

@Rushaway

Copy link
Copy Markdown
Member

Summary

Fixes the TorchRCON server (SourceRCONClient.py, SourceRCONServer.py): replies were never sent, fragmented packets were dropped, auth was weak, and open sessions pinned a stale CommandHandler after a SourceMod reconnect.

1. Replies are actually sent

loop.sock_sendall is a coroutine function, and p_send never awaited it, so no reply reached the socket. That included SERVERDATA_AUTH_RESPONSE, and Python warned coroutine ... was never awaited. send, p_send and ParsePacket are now coroutines and every send is awaited. The auth reply sequence is unchanged: an empty RESPONSE_VALUE, then AUTH_RESPONSE with the request id (or -1 on failure), then the welcome line on success.

An authenticated EXECCOMMAND now also gets an empty RESPONSE_VALUE carrying the request id, which is what the old commented-out line intended. Standard clients that wait for a reply to their command id no longer hang. Command output is still not captured.

2. Per-peer receive buffer and framing

_peer_loop used to overwrite data on every recv, so a packet split across TCP segments (or any command over about 1 KB) was silently lost and the stream desynced. Each peer now keeps a bytearray buffer, and only complete packets are consumed. A partial packet stays buffered until the rest arrives. A declared size outside the Source RCON bounds (< 10 or > 4096) logs a warning and closes the connection. This also bounds the buffer at one maximum-size packet plus one recv chunk. Sizes are read only once 4 bytes are buffered, so struct.error can no longer escape into an untracked task.

3. Auth hardening

  • The password is compared with hmac.compare_digest on UTF-8 bytes. Comparing bytes also keeps non-ASCII passwords working, since compare_digest rejects non-ASCII str.
  • Behavior change: an empty configured password now refuses all authentication. Before, an empty-password client got console access (level 100). Operators who relied on an empty password must set one.
  • SourceRCONServer logs a startup WARNING when the password is empty (RCON auth disabled) or still the shipped default supersecretpassword.
  • A failed auth still closes the connection after the -1 reply. Unauthenticated peers that send nothing are now dropped after 30 s, and concurrent peers are capped at 16. Before, idle sockets could grow peers without bound.

4. No stale CommandHandler

TorchlightHandler.Init() builds a new CommandHandler on every SourceMod reconnect, but each peer captured the one that existed at accept time. Its commands then ran against the dead AsyncClient, and the session kept the whole previous object graph alive. Peers now get a get_command_handler callable (SourceRCONServer.GetCommandHandler), which reads torchlight_handler.command_handler on every command.

5. Task and socket lifecycle

The peer handler and command dispatch use Utils.FireAndForget(coro, self.logger) instead of bare asyncio.Task(...), so exceptions get logged instead of dropped. close() is idempotent and clears the buffer. The socket is closed exactly once, in _peer_handler's finally, on every path: EOF, invalid size, failed auth, auth timeout, and OSError. Remove() tolerates a peer that is already gone. The peer is now added to peers before its task starts.

SourceRCONServer(rcon_config, torchlight_handler) and _server() are unchanged, so cli.py needs no change.

Note: SourceRCONClient.py was stored with CRLF line endings, unlike the rest of the repo. This change normalizes it to LF, so view the diff with "Hide whitespace".

Test plan

  • ruff check ., ruff format --check . and mypy report only the 3 known baseline errors on master (UP041 in FFmpegAudioPlayer.py, arg-type in FlareSolverr.py). No new errors.
  • Throwaway script (not committed): it runs SourceRCONServer on 127.0.0.1 with a stub TorchlightHandler/CommandHandler, drives it with a hand-written struct-packed Source RCON client, and checks:
    • good password (non-ASCII): RESPONSE_VALUE(id), AUTH_RESPONSE(id) and the welcome line are received
    • a 2 KB command sent in 300-byte fragments runs once, with the full text, as the console player, and is answered with RESPONSE_VALUE(id)
    • two packets in one write are both executed
    • after swapping handler.command_handler, the existing session dispatches to the new handler
    • declared size 999999 and size 3 both close the connection without a traceback
    • 2 bytes then EOF closes cleanly (this was the old struct.error path)
    • wrong password: RESPONSE_VALUE(id) and AUTH_RESPONSE(-1), then the connection closes
    • EXECCOMMAND before auth is ignored
    • empty configured password: an empty-password auth is refused and the startup warning is logged
    • shipped default password logs the startup warning
    • the peer cap refuses extra connections, and the auth timeout closes idle unauthenticated peers
    • peers is empty once all clients disconnect
    • no coroutine ... was never awaited warnings (run with -X dev, with RuntimeWarning raised as an error)
  • Smoke test on a live server with a standard RCON client (e.g. rcon-cli)

No VERSION bump (batch policy, see #177).

Closes #185
Part of #177

🤖 Generated with Claude Code

- p_send/ParsePacket are now coroutines and await sock_sendall, so the
  auth response and command replies actually reach the client.
- Each peer keeps a receive buffer and only consumes complete packets;
  a declared size outside 10..4096 closes the connection. struct.error
  can no longer escape on short reads.
- Password compared with hmac.compare_digest on bytes; an empty
  configured password refuses all auth; startup WARNING when the
  password is empty or the shipped default.
- The CommandHandler is resolved through TorchlightHandler at dispatch
  time, so sessions survive a SourceMod reconnect without pinning the
  old object graph.
- Peer and command tasks go through Utils.FireAndForget; sockets are
  closed exactly once; peers capped at 16 and unauthenticated peers
  dropped after 30 s.
- Authenticated EXECCOMMAND packets get an empty RESPONSE_VALUE with the
  request id so standard clients do not hang.

Closes #185

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Rushaway
Rushaway merged commit ab7054e into master Oct 14, 2026
2 checks passed
@Rushaway
Rushaway deleted the fix/185-rcon-replies-framing branch October 14, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SourceRCON: replies are never sent, fragmented packets are dropped, peers keep a stale CommandHandler

1 participant