mark-kubacki / systemd-transparent-udp-forwarderd

Transparent socket-activated UDP Proxy/Forwarder
9 stars 4 forks source link

Optionally shutdown the service (and server) if no activity is detected #3

Open cecton opened 7 months ago

cecton commented 7 months ago

Hello,

I'm currently playing Palworld but the dedicated server does not currently have an option to pause when no one is connected. This consume precious energy for nothing when no one is connected.

I don't know much about C and systemd but I managed to update systemd-transparent-udp-forwarderd to accept an "activity timeout" parameter at the end of its arguments. When provided, if there is nothing on the network during that amount of time (in seconds), then the service stops. I also added a line in the proxy service to allow the stop to be propagated to the actual server.

To test I made a simple UDP server and it worked fine. Unfortunately I couldn't make systemd-transparent-udp-forwarderd work with Palworld, even without my changes. I can see in the status message that some messages are being transferred but the client fails to join with the generic "timeout" error. When I look at the code I see nothing to handle the messages coming back from the game server, could this be related? Maybe I need to add a iptables rule or something?

Anyway... maybe I messed up something or Palworld don't work well with systemd-transparent-udp-forwarderd. But the feature I made does work and might be worth to keep. I'll let you be the judge of that.

Here are my unit files in case you can see something is wrong:

(The dedicated server and proxy are on the same remote machine, only the game client itself is on my machine.)

palworld.service:

[Unit]
Description=Palworld Dedicated Server by A1RM4X 0.3
Wants=network-online.target
After=network-online.target

[Service]
User=steam
Group=steam
Environment="templdpath=$LD_LIBRARY_PATH"
Environment="LD_LIBRARY_PATH=/home/steam/:$LD_LIBRARY_PATH"
Environment="SteamAppId=2394010"
ExecStartPre=/home/steam/palworld-maintenance.sh
ExecStart=/home/steam/.steam/steam/steamapps/common/PalServer/PalServer.sh -useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS > /dev/null
Restart=always
RuntimeMaxSec=4h

[Install]
WantedBy=multi-user.target

palworld-proxy.service:

[Unit]
Description=Palworld Proxy
BindsTo=palworld.service
After=palworld.service
PropagatesStopTo=palworld.service

[Service]
Type=notify
ExecStart=/home/steam/systemd-transparent-udp-forwarderd/systemd-transparent-udp-forwarderd 127.0.0.1:8211
Restart=on-failure

[Install]
WantedBy=default.target

palworld-proxy.socket:

[Socket]
ListenDatagram=0.0.0.0:27000
Transparent=true

[Install]
WantedBy=sockets.target

Test UDP server:

(copy-pasted from the doc mostly https://doc.rust-lang.org/std/net/struct.UdpSocket.html#method.connect )

(The server port is different and the service are somewhat similar.)

use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    let address = "127.0.0.1:34254";
    println!("UDP server started at {address}");
    {
        let socket = UdpSocket::bind(address)?;

        // Receives a single datagram message on the socket. If `buf` is too small to hold
        // the message, it will be cut off.
        let mut buf = [0; 10];

        loop {
            let (amt, src) = socket.recv_from(&mut buf)?;

            let s = String::from_utf8_lossy(&buf[..amt]);
            println!("received: {s:?}");

            if s.trim() == "exit" {
                break;
            }

            // Redeclare `buf` as slice of the received data and send reverse data back to origin.
            let buf = &mut buf[..amt];
            buf.reverse();
            socket.send_to(buf, &src)?;
        }
    } // the socket is closed here

    Ok(())
}

Test commands:

mark-kubacki commented 7 months ago

Hi there!

Thanks for sharing your improvement with others through this PR.

I'm unlikely to find the time to review it properly before the weekend. Apologies for that.

The idea to shutdown the service after idling for some time is excellent. :-) In go, for HTTP servers, that would be the IdleTracker if you ever need sth. like it.

You get "timeouts" as seen by the remotes (PalWorld clients) on missing return packets indeed. And your suspicion is (likely) correct, without an address translation they either get suppressed by your server machine's datacenter, or dismissed by the remotes. iptables could work (iptables snat), but I found tc filter to be the most efficient solution. There's an example in the README. Goes into PreExec= in your systemd unit file. From top of my head:

tc qdisc add dev ext0 ... handle 10: htb
tc filter add dev ext0 parent 10: ... match ip src 10.0.0.0/16 \
  action nat egress 10.0.0.0/16 6.5.4.3
cecton commented 7 months ago

Thanks! I still haven't found a working solution... this whole thing is really out of my league but using tc is yet another step for me. I ended up with this:

sudo modprobe sch_htb

sudo tc qdisc add dev eth0 root handle 1: htb

sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip protocol 17 0xff match u16 0x2093 0xffff at 22 action nat egress 127.0.0.1 10.0.30.125

cecton commented 7 months ago

I made my own non-transparent UDP proxy if you're curious: https://github.com/cecton/systemd-cecile-proxy

mark-kubacki commented 7 months ago

Nice! Release it as binary perhaps, for I fear not everyone will know how to or be inclined to compile it from scratch in a language currently rarely used.

cecton commented 7 months ago

Oh no it's in development, there isnt even a readme

mark-kubacki commented 7 months ago

When you feel comfortable, drop me a line and I’ll list it in the README under “alternatives.” :-)

Going through your code I’ll merge it after promoting the argument to a parameter, called --exit-after-idle or similar. (Maybe there’s a more elegant way of parsing than → https://github.com/systemd/systemd/blob/027d9f90966fba38500530b1fb19defa38d12a03/src/resolve/resolvectl.c#L3684C12-L3684C29 )

Ignore the merge conflict Github could show. I’m working on the console.

cecton commented 7 months ago

You should probably call it --exit-idle-time to stay consistent with systemd's socket-proxy: https://github.com/systemd/systemd/blob/887b2529eb4361838be3260e9b3b0f6c04246699/src/socket-proxy/socket-proxyd.c#L593

cecton commented 7 months ago

I want to add TCP proxy using the splice() method like they do on systemd's socket-proxyd. So I can have a single system unit file with a few sockets of different type for games that have multiple ports. (Apparently lot of games have these RCON port for communicating with the server.)