--- title: "Running RCON Commands from Discord" excerpt: "Send RCON to your CS 1.6 server from a Discord channel: how GoldSrc RCON's challenge-response works over UDP, a minimal bot pattern, and the access controls that keep it from becoming a backdoor." category: "administration" tags: "discord, rcon" image: "" read_time: "9 min read" ---
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.
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 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.
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)
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.
An unrestricted Discord-to-RCON bridge is a remote root shell for your server. Apply all of these:
status, changelevel, amx_kick, say — and reject the rest. Never allow rcon_password, exec, or quit.rcon_password from an environment variable or a 0600 file, never hard-coded in the bot source or committed to git.changelevel.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.
Bad rcon_password in 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.status) span multiple UDP packets. Read in a loop until the socket times out, then concatenate.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.