mirror of
				https://github.com/factoriotools/factorio-docker.git
				synced 2025-10-31 08:58:08 +01:00 
			
		
		
		
	Add rcon client (#550)
* Add rcon client to container * Explain rcon command * Remove test container name * Apply suggestions Co-authored-by: Florian Kinder <florian.kinder@fankserver.com> * Add example docker-compose file * Clarify return code * Switch to pre-update * Update docker-compose.yml Co-authored-by: Florian Kinder <florian.kinder@fankserver.com> * Allow build support for build (/* is only possible in buildx) * Added version information in README.md --------- Co-authored-by: Florian Kinder <florian.kinder@fankserver.com>
This commit is contained in:
		| @@ -98,6 +98,15 @@ docker run -d -it  \ | ||||
| docker attach factorio | ||||
| ``` | ||||
|  | ||||
| ### RCON (2.0.18+) | ||||
|  | ||||
| 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. | ||||
|   | ||||
							
								
								
									
										37
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| 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 | ||||
|      | ||||
|     # Uncomment to enable autoupdate via watchtower | ||||
|     #labels: | ||||
|     #  # Labels to allow watchtower autoupdate only if no players are online | ||||
|     #  - com.centurylinklabs.watchtower.enable=true | ||||
|     #  - com.centurylinklabs.watchtower.scope=factorio | ||||
|     #  - 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 | ||||
|   #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 | ||||
| @@ -1,5 +1,14 @@ | ||||
| FROM debian:stable-slim | ||||
| # build rcon client | ||||
| FROM debian:stable-slim AS rcon-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 +90,7 @@ RUN set -ox pipefail \ | ||||
|  | ||||
| COPY files/*.sh / | ||||
| COPY files/config.ini /opt/factorio/config/config.ini | ||||
| COPY --from=rcon-builder /src/rcon /bin/rcon | ||||
|  | ||||
| VOLUME /factorio | ||||
| EXPOSE $PORT/udp $RCON_PORT/tcp | ||||
|   | ||||
							
								
								
									
										11
									
								
								docker/files/players-online.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								docker/files/players-online.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| PLAYERS=$(rcon /players) | ||||
| ONLINE_COUNT=$(echo "$PLAYERS" | grep -c " (online)$") | ||||
|  | ||||
| if [[ "$ONLINE_COUNT" -gt "0" ]]; then | ||||
|     echo "$PLAYERS" | ||||
|     # exit with 75 (EX_TEMPFAIL) so watchtower skips the update | ||||
|     # https://containrrr.dev/watchtower/lifecycle-hooks/ | ||||
|     exit 75 | ||||
| fi | ||||
							
								
								
									
										13
									
								
								docker/rcon/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								docker/rcon/Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										217
									
								
								docker/rcon/main.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								docker/rcon/main.c
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user