5 minutes
giving rcon to anything
rcon is a TCP/IP protocol that allows server administrators to remotely control servers. it’s been in a majority of multiplayer Source games, as well as now Minecraft, which also explicitly states on the wiki it’s the same protocol.
rcon is an incredibly useful tool. i use it for:
- administration, both automatic and explicit
- relaying messages from Discord channels into games so people can talk with eachother
however… some servers don’t support the rcon protocol. to put them on loudspeaker, the servers in question today are:
- Terraria
- Better than Adventure (at least, as of v7.3_04)
rcon is pretty much essential to me as far as tools to administrate servers goes. however, rcon also has a pretty big flaw: it’s unencrypted.
so, how can we solve both of these problems at the same time?
a lot of these servers, while not supporting rcon, still support issuing commands to the server, just that they only listen to them from stdin. for non-programmers, stdin is the primary input to any given program, usually being keyboard input. if you’ve ran a Minecraft server with -nogui
, you know what stdin is: it’s the way you type into that command prompt window.
however, a lot of people like to run servers with some sort of “daemon” – going off the bare definition of what a daemon is, it’s an attendant, watching your server, restarting it if it crashes. this usually also means that something else other than you will be starting the server, meaning you don’t have access to (or at least, can’t easily access) stdin.
however, there’s still one other way we can write to stdin without having it attached (at least, on Linux systems): named pipes (or “fifo"s).
fifos are special: we can listen for input to a fifo using tail -f
. that’s cool, but not very useful on its’ own. it’s still just going to stdout when we use tail -f
. but we have another trick to turn that into stdin: the shell pipe, or |
.
so, the trick is like so:
- make a fifo using
mkfifo
. this will be referred to as$FIFO
from here on. - run the application,
tail
ing$FIFO
, and piping (|
) that into our server. this becomestail -f "$FIFO" | "$PROGRAM"
. - now, we can indirectly issue commands to our program by writing to
$FIFO
.
want to try it yourself? run this in your Linux terminal: mkfifo ~/fifo; tail -f ~/fifo | sh
. now, open a new terminal and run echo ls > ~/fifo
, and you’ll see the shell you wrote the first command into will immediately run ls
. and you didn’t even have to actually write to stdin!
“ok, that’s cool, but doesn’t solve this whole ‘remote control’ thing.” you’re right! it doesn’t!
my initial idea for actually providing over-the-net remote control using this was to create a shell script that gave you some easy-to-use variables and handled creating the fifo and running everything for you, then listened over $PORT
using nc
(or netcat
). this is cool and all, but horribly overengineered, and still has a couple flaws: it’s unencrypted, and it still can’t respond to commands. honestly, we don’t even really need a script for this. additionally, this script can’t even daemonize correctly; if the server crashes, the daemon running the script will still think everything is ok, because it’s still running. it’s blocking on netcat, not the server running.
so, new plan: daemonize tail -f "$FIFO" | "$PROGRAM"
itself, not the terrible, awful, no-good script. if this short snippet closes, it means "$PROGRAM"
has closed (in this case our server). “but what about remote control?” simple (and thanks to viora for reminding this is even a possibility, and me having forgotten entirely): just use ssh
directly. this handles authentication and encryption at the same time for us. we don’t even need to keep the connection open for this; you can just use ssh host -- "echo command > \"$FIFO\""
, ensuring to lead the command with --
and surrounding it in quotes so we don’t accidentally redirect the output of the command to a local file. note you should only need the escaped double-quotes around $FIFO
if there’s a space in the path.
getting our jank rcon to respond to commands involves listening to stdout. i’ll refer to the path to this file as $STDOUT
. to do this we can actually just write stdout to a file and tail -f
that in the background while we run our command. we can force tail -f
to close by providing it as an argument to timeout 1
, which means it will follow new lines for 1 second then close. this results in…
the commands
tail -f "$FIFO" | "$PROGRAM" >"$STDOUT" 2>&1
tail -f "$FIFO" |
- listen to$FIFO
for new lines, and pipe them into…"$PROGRAM"
- … our program…>"$STDOUT" 2>&1
- … writing all output from the program to$STDOUT
.
OUT=$(ssh host -- 'timeout 1 tail -f -n0 "$STDOUT" & echo "command" > "$FIFO"')
ssh host
- on remote machinehost
…--
- parse the rest of this as input, not options.timeout 1
- … with a limit of 1 second…tail -f -n0 "$STDOUT" &
- … read only new lines from$STDOUT
in the background, while we…echo "command" > "$FIFO"
- write to$FIFO
, which our program is listening to for commands.OUT=$(...)
- put the output into$OUT
for reading – this part is optional.
since we can’t tell exactly what log will be ours, we’re capturing the 1 second of logs around when we issue our command; the result is there may need to be some additional text parsing with something like regex, but it can technically respond now!