From 374f7c8249455233ac8893cb34c5e1df6acba81c Mon Sep 17 00:00:00 2001 From: Tobias Tangemann <tobias@tangemann.org> Date: Sun, 10 Nov 2024 18:47:06 +0100 Subject: [PATCH 1/7] Add rcon client to container --- docker/Dockerfile | 11 +- docker/files/players-online.sh | 11 ++ docker/rcon/Makefile | 13 ++ docker/rcon/main.c | 217 +++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100755 docker/files/players-online.sh create mode 100644 docker/rcon/Makefile create mode 100644 docker/rcon/main.c diff --git a/docker/Dockerfile b/docker/Dockerfile index 2aaec4c..4b1743a 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,13 @@ -FROM debian:stable-slim +FROM debian:stable-slim AS builder +RUN apt-get -q update \ + && DEBIAN_FRONTEND=noninteractive apt-get -qy install build-essential +WORKDIR /src +COPY rcon/* /src +RUN make + +# build factorio image +FROM debian:stable-slim LABEL maintainer="https://github.com/factoriotools/factorio-docker" ARG USER=factorio @@ -81,6 +89,7 @@ RUN set -ox pipefail \ COPY files/*.sh / COPY files/config.ini /opt/factorio/config/config.ini +COPY --from=builder /src/rcon /bin/rcon VOLUME /factorio EXPOSE $PORT/udp $RCON_PORT/tcp diff --git a/docker/files/players-online.sh b/docker/files/players-online.sh new file mode 100755 index 0000000..5a78001 --- /dev/null +++ b/docker/files/players-online.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +PLAYERS=$(docker exec xenodochial_spence rcon /players) +ONLINE_COUNT=$(echo "$PLAYERS" | grep -c " (online)$") + +if [[ "$ONLINE_COUNT" -gt "0" ]]; then + echo "$PLAYERS" + # exit with 75 (EX_TEMPFAIL) for watchtower + # https://containrrr.dev/watchtower/lifecycle-hooks/ + exit 75 +fi diff --git a/docker/rcon/Makefile b/docker/rcon/Makefile new file mode 100644 index 0000000..71131c0 --- /dev/null +++ b/docker/rcon/Makefile @@ -0,0 +1,13 @@ +# Optimization +OPT = -O3 -flto +TARGET = rcon + +CC = gcc +CFLAGS = -std=c17 -Wall -Wextra -pedantic $(OPT) +REMOVE = rm -f + +all: + $(CC) $(CFLAGS) main.c -o $(TARGET) + +clean: + $(REMOVE) $(TARGET) diff --git a/docker/rcon/main.c b/docker/rcon/main.c new file mode 100644 index 0000000..5eb7206 --- /dev/null +++ b/docker/rcon/main.c @@ -0,0 +1,217 @@ +#include <stdio.h> +#include <stdint.h> +#include <stdbool.h> +#include <unistd.h> +#include <netdb.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include <arpa/inet.h> + +#define MIN_PACKET 10 +#define MAX_PACKET 4096 +#define MAX_BODY (MAX_PACKET - (3 * sizeof(uint32_t)) - 2) + +#define RCON_HOST "127.0.0.1" + +typedef enum { + RCON_TYPE_RESPONSE = 0, + RCON_TYPE_EXECCOMMAND = 2, + RCON_TYPE_AUTH_RESPONSE = 2, + RCON_TYPE_AUTH = 3, +} packet_type; + +typedef struct { + uint32_t length; + uint32_t id; + packet_type type; + char body[MAX_BODY]; +} packet; + +int rcon_open(const char *port); +void rcon_create(packet* pkt, packet_type type, const char* body); +bool rcon_send(int rcon_socket, const packet* pkt); +bool rcon_auth(int rcon_socket, const char* password); +bool rcon_recv(int rcon_socket, packet* pkt, packet_type expected_type); +char* combine_args(int argc, char* argv[]); +char* read_password(const char* conf_dir); + +int main(int argc, char* argv[]) { + if (argc < 2) { + fprintf(stderr, "error: missing command argument\n"); + return EXIT_FAILURE; + } + + srand((unsigned int)time(NULL)); + + const char* port = getenv("RCON_PORT"); + if (port == NULL) { + fprintf(stderr, "error: missing $RCON_PORT env\n"); + return EXIT_FAILURE; + } + + const char* conf_dir = getenv("CONFIG"); + if (conf_dir == NULL) { + fprintf(stderr, "error: missing $CONFIG env"); + exit(EXIT_FAILURE); + } + + int rcon_socket = rcon_open(port); + if (rcon_socket == -1) { + fprintf(stderr, "error: could not connect\n"); + return EXIT_FAILURE; + } + + if (!rcon_auth(rcon_socket, read_password(conf_dir))) { + fprintf(stderr, "error: login failed\n"); + return EXIT_FAILURE; + } + + packet pkt; + rcon_create(&pkt, RCON_TYPE_EXECCOMMAND, combine_args(argc, argv)); + if (!rcon_send(rcon_socket, &pkt)) { + fprintf(stderr, "error: send command failed\n"); + return EXIT_FAILURE; + } + + if (rcon_recv(rcon_socket, &pkt, RCON_TYPE_RESPONSE) && pkt.length > 0) { + puts(pkt.body); + } + + return EXIT_SUCCESS; +} + +char* combine_args(int argc, char* argv[]) { + // combine all cli arguments + char* command = malloc(MAX_BODY); + memset(command, 0, MAX_BODY); + strcat(command, argv[1]); + + for (int idx = 2; idx < argc; idx++) { + strcat(command, " "); + strcat(command, argv[idx]); + } + + return command; +} + +char* read_password(const char* conf_dir) { + char* path = malloc(strlen(conf_dir) + 64); + strcpy(path, conf_dir); + strcat(path, "/rconpw"); + + FILE* fptr = fopen(path, "r"); + fseek(fptr, 0, SEEK_END); + long fsize = ftell(fptr); + fseek(fptr, 0, SEEK_SET); /* same as rewind(f); */ + + char *password = malloc(fsize + 1); + fread(password, fsize, 1, fptr); + fclose(fptr); + + password[fsize] = 0; + if (password[fsize-1] == '\n') { + password[fsize-1] = 0; + } + + return password; +} + +int rcon_open(const char *port) { + struct sockaddr_in address = { + .sin_family = AF_INET, + .sin_port = htons(atoi(port)) + }; + inet_aton(RCON_HOST, &address.sin_addr); + + int rcon_socket = socket(AF_INET, SOCK_STREAM, 0); + if (connect(rcon_socket, (struct sockaddr*) &address, sizeof(address)) < 0) { + return -1; + } else { + return rcon_socket; + } +} + +void rcon_create(packet* pkt, packet_type type, const char* body) { + size_t body_length = strlen(body); + if (body_length >= MAX_BODY - 2) { + fprintf(stderr, "error: command to long"); + exit(EXIT_FAILURE); + } + + pkt->id = abs(rand()); + pkt->type = type; + pkt->length = (uint32_t)(sizeof(pkt->id) + sizeof(pkt->type) + body_length + 2); + + memset(pkt->body, 0, MAX_BODY); + strncpy(pkt->body, body, MAX_BODY); +} + +bool rcon_recv(int rcon_socket, packet* pkt, packet_type expected_type) { + memset(pkt, 0, sizeof(*pkt)); + + // Read response packet length + ssize_t expected_length_bytes = sizeof(pkt->length); + ssize_t rx_bytes = recv(rcon_socket, &(pkt->length), expected_length_bytes, 0); + + if (rx_bytes == -1) { + perror("error: socket error"); + return false; + } else if (rx_bytes == 0) { + fprintf(stderr, "error: no data recieved\n"); + return false; + } else if (rx_bytes < expected_length_bytes || pkt->length < MIN_PACKET || pkt->length > MAX_PACKET) { + fprintf(stderr, "error: invalid data\n"); + return false; + } + + ssize_t received = 0; + while (received < pkt->length) { + rx_bytes = recv(rcon_socket, (char *)pkt + sizeof(pkt->length) + received, pkt->length - received, 0); + if (rx_bytes < 0) { + perror("error: socket error"); + return false; + } else if (rx_bytes == 0) { + fprintf(stderr, "error: connection lost\n"); + return false; + } + + received += rx_bytes; + } + + return pkt->type == expected_type; +} + +bool rcon_send(int rcon_socket, const packet* pkt) { + size_t length = sizeof(pkt->length) + pkt->length; + char *ptr = (char*) pkt; + + while (length > 0) { + ssize_t ret = send(rcon_socket, ptr, length, 0); + + if (ret == -1) { + return false; + } + + ptr += ret; + length -= ret; + } + + return true; +} + +bool rcon_auth(int rcon_socket, const char* password) { + packet pkt; + rcon_create(&pkt, RCON_TYPE_AUTH, password); + + if (!rcon_send(rcon_socket, &pkt)) { + return false; + } + + if (!rcon_recv(rcon_socket, &pkt, RCON_TYPE_AUTH_RESPONSE)) { + return false; + } + + return true; +} From ca85d49187c6016528acdae5afb7431eea20742a Mon Sep 17 00:00:00 2001 From: Tobias Tangemann <tobias@tangemann.org> Date: Sun, 10 Nov 2024 18:44:05 +0100 Subject: [PATCH 2/7] Explain rcon command --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 7387068..d65db66 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,15 @@ docker run -d -it \ docker attach factorio ``` +### RCON + +Alternativly (e.g. for scripting) the RCON connection can be used to send commands to the running factorio server. +This does not require the RCON connection to be exposed. + +```shell +docker exec factorio rcon /h +``` + ### Upgrading Before upgrading backup the save. It's easy to make a save in the client. From 16f2df493455e75a44686646c15db8bb59940356 Mon Sep 17 00:00:00 2001 From: Tobias Tangemann <tobias@tangemann.org> Date: Sun, 10 Nov 2024 19:00:31 +0100 Subject: [PATCH 3/7] Remove test container name --- docker/files/players-online.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/files/players-online.sh b/docker/files/players-online.sh index 5a78001..aa2444d 100755 --- a/docker/files/players-online.sh +++ b/docker/files/players-online.sh @@ -1,6 +1,6 @@ #!/bin/bash -PLAYERS=$(docker exec xenodochial_spence rcon /players) +PLAYERS=$(rcon /players) ONLINE_COUNT=$(echo "$PLAYERS" | grep -c " (online)$") if [[ "$ONLINE_COUNT" -gt "0" ]]; then From e6e53c35a1df1085de666f6da3235759fd9808a9 Mon Sep 17 00:00:00 2001 From: Tobias <tobias@tangemann.org> Date: Mon, 11 Nov 2024 13:25:45 +0100 Subject: [PATCH 4/7] Apply suggestions Co-authored-by: Florian Kinder <florian.kinder@fankserver.com> --- docker/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4b1743a..d03f8bf 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM debian:stable-slim AS builder +# build rcon client +FROM debian:stable-slim AS rcon-builder RUN apt-get -q update \ && DEBIAN_FRONTEND=noninteractive apt-get -qy install build-essential @@ -89,7 +90,7 @@ RUN set -ox pipefail \ COPY files/*.sh / COPY files/config.ini /opt/factorio/config/config.ini -COPY --from=builder /src/rcon /bin/rcon +COPY --from=rcon-builder /src/rcon /bin/rcon VOLUME /factorio EXPOSE $PORT/udp $RCON_PORT/tcp From 8d972fdf696cd387b6f4010aecb31e9c0f5d4fb9 Mon Sep 17 00:00:00 2001 From: Tobias Tangemann <tobias@tangemann.org> Date: Wed, 13 Nov 2024 22:47:04 +0100 Subject: [PATCH 5/7] Add example docker-compose file --- docker-compose.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8d07bf6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: "2" +services: + factorio: + container_name: factorio + image: factoriotools/factorio:stable + restart: unless-stopped + ports: + - "34197:34197/udp" + - "27015:27015/tcp" + volumes: + - ./data:/factorio + environment: + - UPDATE_MODS_ON_START=true + #labels: + # # Labels to allow autoupdate only if no players are online + # - com.centurylinklabs.watchtower.enable=true + # - com.centurylinklabs.watchtower.scope=factorio + # - com.centurylinklabs.watchtower.lifecycle.pre-check="/players-online.sh" + + # Uncomment the following files to use watchtower for updating the factorio container + # Full documentation of watchtower: https://github.com/containrrr/watchtower + #watchtower: + # container_name: watchtower_factorio + # image: containrrr/watchtower + # restart: unless-stopped + # volumes: + # - /var/run/docker.sock:/var/run/docker.sock + # environment: + # # Only update containers which have the option 'watchtower.enable=true' set + # - WATCHTOWER_TIMEOUT=30s + # - WATCHTOWER_LABEL_ENABLE=true + # - WATCHTOWER_POLL_INTERVAL=3600 + # - WATCHTOWER_LIFECYCLE_HOOKS=true + # labels: + # - com.centurylinklabs.watchtower.scope=factorio From 725018f8b4327c411c69855139e1b01e4ca28be2 Mon Sep 17 00:00:00 2001 From: Tobias Tangemann <tobias@tangemann.org> Date: Wed, 13 Nov 2024 22:47:55 +0100 Subject: [PATCH 6/7] Clarify return code --- docker/files/players-online.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/files/players-online.sh b/docker/files/players-online.sh index aa2444d..5814b74 100755 --- a/docker/files/players-online.sh +++ b/docker/files/players-online.sh @@ -5,7 +5,7 @@ ONLINE_COUNT=$(echo "$PLAYERS" | grep -c " (online)$") if [[ "$ONLINE_COUNT" -gt "0" ]]; then echo "$PLAYERS" - # exit with 75 (EX_TEMPFAIL) for watchtower + # exit with 75 (EX_TEMPFAIL) so watchtower skips the update # https://containrrr.dev/watchtower/lifecycle-hooks/ exit 75 fi From 54a03517ba33db85c5f85fb63d578ba2874463c1 Mon Sep 17 00:00:00 2001 From: Tobias Tangemann <tobias@tangemann.org> Date: Wed, 13 Nov 2024 22:53:58 +0100 Subject: [PATCH 7/7] Switch to pre-update --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8d07bf6..a3e7e8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: # # Labels to allow autoupdate only if no players are online # - com.centurylinklabs.watchtower.enable=true # - com.centurylinklabs.watchtower.scope=factorio - # - com.centurylinklabs.watchtower.lifecycle.pre-check="/players-online.sh" + # - com.centurylinklabs.watchtower.lifecycle.pre-update="/players-online.sh" # Uncomment the following files to use watchtower for updating the factorio container # Full documentation of watchtower: https://github.com/containrrr/watchtower