--- title: "Sending RCON Commands from the Linux Command Line" excerpt: "How the GoldSrc RCON challenge-response protocol works over UDP, and a self-contained Python script to run rcon commands against a CS 1.6 server from a shell." category: "administration" tags: "rcon, cli" image: "" read_time: "9 min read" ---
Being able to fire an RCON command at your CS 1.6 server from a shell — changelevel, amx_ban, a config reload — is enormously handy for scripts, cron jobs and quick fixes without launching the game. But GoldSrc RCON is not the TCP protocol that Source-era tools like mcrcon speak; it is a UDP challenge-response scheme, and using the wrong tool just times out. This explains the protocol and gives you a working client.
GoldSrc RCON runs over the same UDP game port (default 27015) and works in two steps:
getchallenge request. The server replies with a one-time challenge number.rcon <challenge> "<password>" <command>. The server runs it and returns the console output.Every packet is prefixed with four 0xFF bytes. Because it is UDP with a challenge, TCP-based RCON tools (built for Source or Minecraft) cannot talk to it — that is the number-one reason people think "RCON is broken."
RCON must be enabled with a password in server.cfg:
rcon_password "USE-A-LONG-RANDOM-STRING"
Because RCON is brute-forceable over the network, make the password long and random, and never expose the game port to the whole internet if you can restrict it — see which ports to open. On ReHLDS you also get failed-attempt limits worth enabling.
This implements the two-step handshake with only the standard library:
#!/usr/bin/env python3
import socket, sys
def rcon(host, port, password, command):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(4)
s.connect((host, port))
# 1) get a challenge
s.send(b"\xFF\xFF\xFF\xFFgetchallenge\n")
reply = s.recv(1400).decode("latin-1", "ignore")
# reply looks like: \xFF\xFF\xFF\xFFchallenge rcon 1234567890
challenge = reply.split("rcon")[1].strip()
# 2) send the command
pkt = 'rcon %s "%s" %s' % (challenge, password, command)
s.send(b"\xFF\xFF\xFF\xFF" + pkt.encode("latin-1") + b"\n")
return s.recv(4096).decode("latin-1", "ignore")
if __name__ == "__main__":
host, port, pw = sys.argv[1], int(sys.argv[2]), sys.argv[3]
cmd = " ".join(sys.argv[4:])
print(rcon(host, port, pw, cmd).lstrip("\xFF").lstrip("l"))
Save it as rcon.py, make it executable, and call it:
chmod +x rcon.py ./rcon.py 127.0.0.1 27015 "your-rcon-password" status ./rcon.py 127.0.0.1 27015 "your-rcon-password" changelevel de_inferno
The reply is prefixed with 0xFF 0xFF 0xFF 0xFF l; the script strips that so you get clean console text.
./rcon.py HOST 27015 PW "status" // players, map, uptime ./rcon.py HOST 27015 PW "amx_map de_nuke" // change map via AMXX ./rcon.py HOST 27015 PW "exec server.cfg" // reload config ./rcon.py HOST 27015 PW "amx_reloadadmins" // re-read users.ini
Quote a command with arguments as one shell argument if it contains spaces you want kept together, e.g. "say hello everyone". This is the backbone of remote admin scripting — pair it with the uptime monitor to auto-changelevel off a stuck map, or with the log parser to react to events.
A password on the command line lands in ~/.bash_history and in ps output. For anything recurring, read it from an environment variable or a mode-600 file instead of typing it inline, and restrict who can run the script.
Bad rcon_password — the password is wrong or was rotated. It must match rcon_password exactly, including case.Bad challenge — you reused an old challenge; each command needs a fresh getchallenge. The script does this every call.changelevel) produce no console text; that is normal. Run status to confirm the channel works.Run status against the server and confirm you get the player list and current map back:
./rcon.py 127.0.0.1 27015 "your-rcon-password" status
A clean hostname, map and player table means the handshake and password are correct. Then run a harmless state change like exec server.cfg and confirm from the game console that it took. Once the script works locally, you can safely wire it into cron and monitoring.