Implementing Systemd Socket Activation for Valheim Dedicated Server
Posted on .
This is a post about how I set up the Valheim dedicated server for socket activation with Systemd, so that it can be started and shut down on demand. This helps it use less resources, keeping the server and the environment happier. If you just want to grab the codes, you might skip directly to the repository, but keep in mind that it requires understanding of Systemd and Linux system administration.
Background
We've recently gotten into Valheim and I was wondering how to best set up a dedicated server for my friends. To be specific, setting up the server is easy and I found good guides on how to do it (Adam Hurm's and especially the one in the Fandom Valheim wiki). As a lazy man I didn't want to suffice with starting and stopping the server manually, and I didn't want it running unnecessarily all the time – especially given its high idle CPU usage.
My first thought was creating a web service where one could start and stop the server at will. This would have sufficed, but could we do something even better? Could we have it automatically start and stop?
Socket activation
Systemd has a cool feature called socket activation. This basically means that instead of a service running all the time and listening to a specific port, we tell Systemd to listen for traffic instead, and start the service on demand. With this feature, our Valheim server can start the moment someone connects to it.
The problem? Valheim's dedicated server is not built with this feature in mind. I wouldn't expect it to, either, since it needs specially setting up the socket code to bind to a file descriptor received from Systemd instead of an address and port like normal.
The usual answer to this is systemd-socket-proxyd that comes bundled with Systemd. It can take those Systemd file descriptors and proxy traffic bidirectionally between the sockets and the app you are running. With some unit file magic explained later, you can link the proxy and the original service so that the proxy starting and stopping also starts and stops the original service.
Unfortunately systemd-socket-proxyd does not support UDP. There is some prior art for bypassing this restriction by using a custom proxy, such as
- mark-kubacki/systemd-transparent-udp-forwarderd,
- SharkBaitDLS/systemd-udp-proxy, or even
- ryancdotorg/libsdsock that would actually alter the socket opening via
LD_PRELOAD
so that it instead opens the Systemd provided socket.
In the end I didn't go for any of these (which could be a dumb decision). I spent an evening fighting with socat not managing to get any further, and then figured what the heck, why don't I write a proxy myself? How hard could it be? Turns out, not too hard, but it's annoying to squeeze all the bugs out.
Writing my own proxy
My first thought was, of course, using Erlang (via Gleam). Turns out Erlang's gen_udp can't use a Systemd socket since Systemd has already bound to the socket. So being a higher level guy, I opted for Node.js as it's easily installable and the event based architecture is quite suitable for this kind of work, even if it is only single threaded.
The end result is Valproxy, a small single-file UDP proxy in JavaScript. In theory it can be used as a proxy for any kind of application, but it has only been tested with Valheim's dedicated server.
My first thought was to have the one socket from Systemd and one socket that connects to Valheim, and then to just keep passing messages between them. After thinking for a moment, of course it would not be so simple: when you send a message to Valheim and receive a reply, who do you send that reply to? The UDP packet is pointed at the proxy, not the original client. Node.js, as a higher level language, does not have the tools to forge the sender of a packet, so since your proxy is the one sending (forwarding) the packets to Valheim, the Valheim server will point all the replies to the proxy. Additionally, since UDP is a connectionless protocol, there is no way to correlate a certain packet to its reply.
But saying "connectionless" is a bit of a misnomer. When a UDP packet arrives via Systemd to the Node.js proxy, it has
an identifying pair of information: the remote client address and port. In case you didn't know, it's not only the
server that has a port that it uses for communication, the client will also choose a port (usually at random). And if
your packets arrived from address 1.2.3.4:54321
, then sending a reply through the same socket to address
1.2.3.4:54321
will deliver that reply to the original client. This is how replies are implemented with UDP, and
is a kind of a "connection" that we need.
We still have the problem of figuring out which Valheim reply is for which sent packet. To fix this, instead of a single
socket, we will open one socket per remote client address/port combination. In other words, 1.2.3.4:54321
will cause
one connection to be opened, 123.123.123.123:33720
will cause another, and so on. Inside the proxy, we store which
socket was created for which address–port pair. When a packet from Valheim is received in one of the sockets, we now
know the correct remote address and port to send it to.
As a text chart, it would look something like the following:
The rest of the code basically deals with managing the sockets, like closing them when they have been inactive for a long enough time, and closing the entire proxy when there have been no clients for a while.
In keeping with the spirit of UDP, the proxy discards all traffic that didn't make it through the proxy. E.g. if the target is not running and a "connection refused" error is returned, that packet is then lost. At first I implemented a buffer to store and replay packets, but it wasn't reliable and it could mess with the target app's own retry logic.
Sidetrack to dual stack addresses
On my server, even though I'm using udp4
as the socket type, the remote address I received from Node.js was an
IPv4-mapped IPv6 address. Essentially it was an IP that
looked like this: ::ffff:1.2.3.4
. When sending a packet back to that address, it was lost in transit and true to UDP's
style, there was no error. This meant that a remote client could talk to my Valheim server, but the server couldn't talk
back. The solution I found for this was to remove the leading ::ffff:
from an address if it exists.
Systemd magic
Now that I had the proxying working, it was time to set up Systemd to manage the starting and stopping of the server. This is done with socket activation and setting certain dependencies between the services. There are three units in total:
valheim.service
, which is the Valheim server itself,valheim-proxy.service
, which is the proxy between the Systemd socket and the Valheim server, andvalheim-proxy.socket
, the socket used for incoming connections.
Let's start with valheim.service
. Note that all of these examples are also included in the code repository for your
convenience.
[Unit]
Description=Valheim server
StopWhenUnneeded=yes
[Service]
Type=simple
WorkingDirectory=/home/valheim/valheim
Environment=LD_LIBRARY_PATH=./linux64
Environment=SteamAppID=892970
ExecStart=/home/valheim/valheim/valheim_server.x86_64 -name "server_name" -port 2456 -nographics -batchmode -world "world_name" -password "password" -public 0
KillSignal=SIGINT
WatchdogSignal=SIGINT
The service section is the usual Valheim dedicated server startup stuff, except for the final two lines.
They're very important because the Valheim server uses SIGINT
to start a clean shutdown. Without these, SIGTERM
would be sent instead, leading to the immediate termination of the server and loss of the current game progress. In a
clean shutdown, the server will save the current world state properly.
Note that I'm running this as a user level Systemd service. If I was using the root level, I'd consider additional
safeguards like PrivateNetwork=yes
. I found this
Red Hat article about Systemd sandboxing good reading about that.
Next is the proxy's service file:
[Unit]
BindsTo=valheim.service
After=valheim.service
Requires=valheim-proxy.socket
After=valheim-proxy.socket
[Service]
Type=simple
WorkingDirectory=/home/valheim/valproxy
ExecStart=/home/valheim/.asdf/shims/node index.js 127.0.0.1 2456
The important detail here is BindsTo
, which binds the lifetime of the Valheim server and the proxy together, as they
cannot live without one another. I actually don't know if StopWhenUnneeded=yes
in the Valheim server unit is needed
when this is set, but it doesn't seem to hurt either.
I'm using asdf to manage the version of Node.js on the server, which explains the weird binary path.
The final piece we need is the socket:
[Socket]
ListenDatagram=12345
[Install]
WantedBy=sockets.target
And there we have it. Once we do systemctl --user enable --now valheim-proxy.socket
, Systemd will open that port and
start the Valheim server once there's traffic on the port. When there haven't been connections in a while, the server
will again be shut down automatically.
Caveat emptor
The Valheim server takes around 35 seconds to start on my small server. This is too long for the Valheim client, which gives up before the server is accepting connections. This means that the first one to connect to the server after it has been shut down needs to connect twice. Unfortunately I don't see a way to bypass this, since the timeouts in the client cannot be modified.
Another obvious caveat is that this has not been thoroughly tested. Please take regular backups of your Valheim world! I'm using this with my friends and it seems to work, but we haven't yet stress tested it with lots of users and all kinds of in-game situations. So take this with a heavy "your mileage may vary" warning.