itzg / docker-minecraft-server

Docker image that provides a Minecraft Server that will automatically download selected version at startup
https://docker-minecraft-server.readthedocs.io/
Apache License 2.0
9.29k stars 1.53k forks source link

improve autopause port detection #1545

Open atfrase opened 2 years ago

atfrase commented 2 years ago

Enhancement Type

Improve an existing feature

Describe the enhancement

Currently, autopause starts with a template /autopause/knockd-config.cfg file and then substitutes SERVER_PORT and RCON_PORT in case they've been changed. The config also includes port 19132/udp (the default Bedrock port) just in case someone installs GeyserMC, but if they were to run that on a different port, there's no way for them to make that port change in the knockd config.

Some other plugins also add server ports that ought to prevent or resume from pause, such as the Dynmap plugin with its built-in web server, and there's similarly no easy way to add those ports to the knockd config so that connections to the map port (8123 by default) wake up the server.

To handle both of these cases, I wonder if it would be worth just generating the entire knockd-config.cfg file dynamically on startup, by having the container ask docker which ports have been mapped into it and adding knockd listeners for each one. Anyone who changes a port (such as for GeyserMC), or adds a plugin that needs a new port (such as Dynmap) must necessarily add that port mapping to their docker-compose.yml, so it should suffice for knockd to just listen on all of those ports (plus RCON).

Ideally (but maybe optionally) it would be great to somehow also detect activity on any such additional ports (such as someone watching Dynmap) to prevent server pause even if no players are connected any more. I'm not sure how best to do that, though, and that maybe should have to be explicitly set (i.e. env AUTOPAUSE_TCP_PORTS) rather than being automatic for every port mapped to the container.

atfrase commented 2 years ago

I tinkered with this today and hacked together something that seems to work for me so far. Rather than fork and rebuild the entire image I just added an entrypoint to a custom script (below), so that on container startup it patches parts of the autopause setup and then calls the original /start entrypoint. Consequently I can't offer you a proper PR for this, but at least the changes are relatively small. :)

First, the script scans any (space-delimited) port numbers in the AUTOPAUSE_TCP_PORTS and AUTOPAUSE_UDP_PORTS env vars, and for each one, appends a new section to /autopause/knockd-config.cfg; that lets me set AUTOPAUSE_TCP_PORTS=8123 so that queries to the Dynmap web server port will wake the java server process.

Next, the script appends /autopause/autopause-fcns.sh to redefine java_clients_connected() such that any ESTABLISHED tcp connections on any AUTOPAUSE_TCP_PORTS as shown in netstat will be counted as active clients; that prevents the server from pausing so long as someone is actively watching the Dynmap (but if I even switch to another browser tab, those connections promptly disappear and the server starts counting down to pause again). This piece is less important for my use case since even if the server paused while someone was watching Dynmap, the javascript polling in the client's browser would just hit that port and resume the server again right away anyway, but in general there may be use cases that react more poorly to interruptions and would care more about making sure not to pause at all so long as a tcp connection remained established.

#!/bin/bash

: "${AUTOPAUSE_TCP_PORTS:=}"
: "${AUTOPAUSE_UDP_PORTS:=}"
export AUTOPAUSE_TCP_PORTS
export AUTOPAUSE_UDP_PORTS

# add listening ports to the template knockd config
AUTOPAUSE_TCP_PORTS_RE=""
for P in $AUTOPAUSE_TCP_PORTS ; do
    if ! [[ $P =~ ^[0-9]+$ ]]; then
        log "Warning: ignoring invalid port '$P' in AUTOPAUSE_TCP_PORTS"
    else
        AUTOPAUSE_TCP_PORTS_RE="$AUTOPAUSE_TCP_PORTS_RE|$P"
        cat >> /autopause/knockd-config.cfg <<- EOF
[unpauseMCServer-tcp$P]
 sequence = $P:tcp
 seq_timeout = 1
 command = /autopause/resume.sh
 tcpflags = syn
EOF
    fi
done
for P in $AUTOPAUSE_UDP_PORTS ; do
    if ! [[ $P =~ ^[0-9]+$ ]]; then
        log "Warning: ignoring invalid port '$P' in AUTOPAUSE_UDP_PORTS"
    else
        cat >> /autopause/knockd-config.cfg <<- EOF
[unpauseMCServer-udp$P]
 sequence = $P:udp
 seq_timeout = 1
 command = /autopause/resume.sh
EOF
    fi
done
export AUTOPAUSE_TCP_PORTS_RE

# patch the autopause functions to check for additional TCP port connections
cat >> /autopause/autopause-fcns.sh <<- EOF

tcp_clients_connections() {
  netstat --numeric --all --tcp | awk '(\$6=="ESTABLISHED" && \$4~":(${AUTOPAUSE_TCP_PORTS_RE:1})\$")' | wc -l
}

java_clients_connected() {
  (( \$(java_clients_connections) + \$(tcp_clients_connections) > 0 ))
}
EOF

# call the original container entrypoint
/start $*
itzg commented 2 years ago

Great idea on allowing for runtime building of the config file. I will definitely "borrow" some of the logic you provided 😉

taggart commented 1 year ago

Maybe this helps solve #1630 as well?

Caedis commented 1 year ago

Would definitely solve my issue (i opened 1630) with port scanners waking up a paused server that I host for some friends since I could just set AUTOPAUSE_TCP_PORTS=25565,25565,25565,25565 and along with #2003, a custom sequence timeout

Actually, rereading the code block, the above would just create 4 [unpauseMCServer-tcp25565] blocks