Running RCON from Discord means your admins can kick, change the map, or read status from their phone without opening a game client. It is genuinely useful — and genuinely dangerous, because you are exposing the most powerful control channel your server has to a chat app. Done carefully it is fine; done carelessly it hands your rcon_password to anyone who compromises the bot. This explains how GoldSrc RCON actually works and how to bridge it safely.
1. Understand GoldSrc RCON (it is UDP, not Source RCON)
CS 1.6 does not use the TCP Source RCON protocol that newer Source games use. Its RCON runs over the same UDP game port (default 27015) and is challenge-based. The exchange is:
- The client sends a challenge request to the server.
- The server replies with a numeric challenge token.
- The client sends the actual command with the challenge and the password.
The raw packets look like this (each prefixed with four 0xFF bytes):
--> \xFF\xFF\xFF\xFF getchallenge rcon\n <-- \xFF\xFF\xFF\xFF challenge rcon 1234567890\n --> \xFF\xFF\xFF\xFF rcon 1234567890 "your_rcon_password" status\n <-- \xFF\xFF\xFF\xFF ...command output...
The password travels in cleartext inside the UDP payload, so anyone who can sniff the path can read it. That single fact drives every security decision below.
2. The bot architecture
You need a small always-on process that: listens to a Discord channel, authenticates the sender, translates an allowed message into an RCON command, sends it via the challenge-response above, and posts the reply back. Keep the bot on the same host or network as the game server so the cleartext RCON never crosses the public internet.
Discord channel --> bot (Discord API) --> UDP RCON --> CS 1.6 server
(localhost / LAN)
3. A minimal RCON sender
The core is a few lines of socket code. This Python function does the two-step challenge and returns the output — the exact same logic any language uses:
import socket
def rcon(host, port, password, command, timeout=3):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(timeout)
s.connect((host, port))
# 1) ask for a challenge
s.send(b"\xff\xff\xff\xff" + b"getchallenge rcon\n")
reply = s.recv(1400).decode("latin-1", "ignore")
token = reply.split("challenge rcon ")[1].split()[0]
# 2) send the command with challenge + password
packet = b"\xff\xff\xff\xff" + (
'rcon %s "%s" %s\n' % (token, password, command)
).encode("latin-1")
s.send(packet)
out = s.recv(4096).decode("latin-1", "ignore")
s.close()
return out.lstrip("\xff").lstrip("l") # strip packet header
Wire that into whatever Discord library you prefer (discord.py, discord.js) so an allowed message calls rcon(...) and posts the return value. The Discord half is boilerplate; the security half is not.
4. Lock it down — this is the whole point
An unrestricted Discord-to-RCON bridge is a remote root shell for your server. Apply all of these:
- One private channel, restricted by Discord role. The bot only reads a specific admin channel and only from users with a specific role. Ignore everything else.
- Whitelist commands. Do not forward arbitrary text. Allow a fixed set —
status,changelevel,amx_kick,say— and reject the rest. Never allowrcon_password,exec, orquit. - Keep the password out of the code. Load
rcon_passwordfrom an environment variable or a0600file, never hard-coded in the bot source or committed to git. - Colocate the bot. Run it on the game host or same LAN so RCON stays off the public internet. If it must be remote, tunnel it (SSH/WireGuard) — do not send cleartext RCON across the internet.
- Rate-limit and log. Log every command with the Discord user who sent it, and throttle, so a compromised account cannot spam
changelevel.
5. Rotate the password if it ever leaks
Because RCON is cleartext and now also lives in a bot config, treat the password as exposed the moment anything goes wrong — a leaked config, a kicked admin, a suspicious log entry. Change rcon_password in server.cfg, reload, and update the bot. A long random password is assumed; see the RCON notes in the essential cvars list.
Common errors
- Bot gets no reply / timeout — wrong port, a firewall dropping UDP, or you tried Source-style TCP RCON. GoldSrc RCON is UDP on the game port; open it only to the bot host.
Bad rcon_passwordin the reply — the password is wrong, or you did not quote it in the packet. It must be wrapped in double quotes.Bad challenge— you reused an old challenge token or waited too long; request a fresh challenge for every command.- Output is truncated — long replies (a full
status) span multiple UDP packets. Read in a loop until the socket times out, then concatenate. - Anyone in the server can run commands — you did not restrict the channel/role or whitelist commands. Fix this before you deploy, not after.
Verification
Test the sender from a shell on the bot host before touching Discord:
python3 -c "import bot; print(bot.rcon('127.0.0.1',27015,'PW','status'))"
A correct setup prints the status block with hostname, map and player list. Then, from Discord, send an allowed command as an authorized user and confirm the reply posts; send a forbidden command and confirm it is rejected and logged. Finally, send from an unauthorized account and confirm the bot ignores it entirely — that silent rejection is the test that matters most.









