Skip to content

Commit c2d7c56

Browse files
trippletfank
andauthored
Add rcon client (#550)
* Add rcon client to container * Explain rcon command * Remove test container name * Apply suggestions Co-authored-by: Florian Kinder <[email protected]> * Add example docker-compose file * Clarify return code * Switch to pre-update * Update docker-compose.yml Co-authored-by: Florian Kinder <[email protected]> * Allow build support for build (/* is only possible in buildx) * Added version information in README.md --------- Co-authored-by: Florian Kinder <[email protected]>
1 parent f14bedb commit c2d7c56

File tree

6 files changed

+298
-1
lines changed

6 files changed

+298
-1
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ docker run -d -it \
9898
docker attach factorio
9999
```
100100

101+
### RCON (2.0.18+)
102+
103+
Alternativly (e.g. for scripting) the RCON connection can be used to send commands to the running factorio server.
104+
This does not require the RCON connection to be exposed.
105+
106+
```shell
107+
docker exec factorio rcon /h
108+
```
109+
101110
### Upgrading
102111

103112
Before upgrading backup the save. It's easy to make a save in the client.

docker-compose.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
version: "2"
2+
services:
3+
factorio:
4+
container_name: factorio
5+
image: factoriotools/factorio:stable
6+
restart: unless-stopped
7+
ports:
8+
- "34197:34197/udp"
9+
- "27015:27015/tcp"
10+
volumes:
11+
- ./data:/factorio
12+
environment:
13+
- UPDATE_MODS_ON_START=true
14+
15+
# Uncomment to enable autoupdate via watchtower
16+
#labels:
17+
# # Labels to allow watchtower autoupdate only if no players are online
18+
# - com.centurylinklabs.watchtower.enable=true
19+
# - com.centurylinklabs.watchtower.scope=factorio
20+
# - com.centurylinklabs.watchtower.lifecycle.pre-update="/players-online.sh"
21+
22+
# Uncomment the following files to use watchtower for updating the factorio container
23+
# Full documentation of watchtower: https://github.com/containrrr/watchtower
24+
#watchtower:
25+
# container_name: watchtower_factorio
26+
# image: containrrr/watchtower
27+
# restart: unless-stopped
28+
# volumes:
29+
# - /var/run/docker.sock:/var/run/docker.sock
30+
# environment:
31+
# # Only update containers which have the option 'watchtower.enable=true' set
32+
# - WATCHTOWER_TIMEOUT=30s
33+
# - WATCHTOWER_LABEL_ENABLE=true
34+
# - WATCHTOWER_POLL_INTERVAL=3600
35+
# - WATCHTOWER_LIFECYCLE_HOOKS=true
36+
# labels:
37+
# - com.centurylinklabs.watchtower.scope=factorio

docker/Dockerfile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
FROM debian:stable-slim
1+
# build rcon client
2+
FROM debian:stable-slim AS rcon-builder
3+
RUN apt-get -q update \
4+
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install build-essential
25

6+
WORKDIR /src
7+
COPY rcon/ /src
8+
RUN make
9+
10+
# build factorio image
11+
FROM debian:stable-slim
312
LABEL maintainer="https://github.com/factoriotools/factorio-docker"
413

514
ARG USER=factorio
@@ -81,6 +90,7 @@ RUN set -ox pipefail \
8190

8291
COPY files/*.sh /
8392
COPY files/config.ini /opt/factorio/config/config.ini
93+
COPY --from=rcon-builder /src/rcon /bin/rcon
8494

8595
VOLUME /factorio
8696
EXPOSE $PORT/udp $RCON_PORT/tcp

docker/files/players-online.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
PLAYERS=$(rcon /players)
4+
ONLINE_COUNT=$(echo "$PLAYERS" | grep -c " (online)$")
5+
6+
if [[ "$ONLINE_COUNT" -gt "0" ]]; then
7+
echo "$PLAYERS"
8+
# exit with 75 (EX_TEMPFAIL) so watchtower skips the update
9+
# https://containrrr.dev/watchtower/lifecycle-hooks/
10+
exit 75
11+
fi

docker/rcon/Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Optimization
2+
OPT = -O3 -flto
3+
TARGET = rcon
4+
5+
CC = gcc
6+
CFLAGS = -std=c17 -Wall -Wextra -pedantic $(OPT)
7+
REMOVE = rm -f
8+
9+
all:
10+
$(CC) $(CFLAGS) main.c -o $(TARGET)
11+
12+
clean:
13+
$(REMOVE) $(TARGET)

docker/rcon/main.c

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#include <stdio.h>
2+
#include <stdint.h>
3+
#include <stdbool.h>
4+
#include <unistd.h>
5+
#include <netdb.h>
6+
#include <stdlib.h>
7+
#include <string.h>
8+
#include <time.h>
9+
10+
#include <arpa/inet.h>
11+
12+
#define MIN_PACKET 10
13+
#define MAX_PACKET 4096
14+
#define MAX_BODY (MAX_PACKET - (3 * sizeof(uint32_t)) - 2)
15+
16+
#define RCON_HOST "127.0.0.1"
17+
18+
typedef enum {
19+
RCON_TYPE_RESPONSE = 0,
20+
RCON_TYPE_EXECCOMMAND = 2,
21+
RCON_TYPE_AUTH_RESPONSE = 2,
22+
RCON_TYPE_AUTH = 3,
23+
} packet_type;
24+
25+
typedef struct {
26+
uint32_t length;
27+
uint32_t id;
28+
packet_type type;
29+
char body[MAX_BODY];
30+
} packet;
31+
32+
int rcon_open(const char *port);
33+
void rcon_create(packet* pkt, packet_type type, const char* body);
34+
bool rcon_send(int rcon_socket, const packet* pkt);
35+
bool rcon_auth(int rcon_socket, const char* password);
36+
bool rcon_recv(int rcon_socket, packet* pkt, packet_type expected_type);
37+
char* combine_args(int argc, char* argv[]);
38+
char* read_password(const char* conf_dir);
39+
40+
int main(int argc, char* argv[]) {
41+
if (argc < 2) {
42+
fprintf(stderr, "error: missing command argument\n");
43+
return EXIT_FAILURE;
44+
}
45+
46+
srand((unsigned int)time(NULL));
47+
48+
const char* port = getenv("RCON_PORT");
49+
if (port == NULL) {
50+
fprintf(stderr, "error: missing $RCON_PORT env\n");
51+
return EXIT_FAILURE;
52+
}
53+
54+
const char* conf_dir = getenv("CONFIG");
55+
if (conf_dir == NULL) {
56+
fprintf(stderr, "error: missing $CONFIG env");
57+
exit(EXIT_FAILURE);
58+
}
59+
60+
int rcon_socket = rcon_open(port);
61+
if (rcon_socket == -1) {
62+
fprintf(stderr, "error: could not connect\n");
63+
return EXIT_FAILURE;
64+
}
65+
66+
if (!rcon_auth(rcon_socket, read_password(conf_dir))) {
67+
fprintf(stderr, "error: login failed\n");
68+
return EXIT_FAILURE;
69+
}
70+
71+
packet pkt;
72+
rcon_create(&pkt, RCON_TYPE_EXECCOMMAND, combine_args(argc, argv));
73+
if (!rcon_send(rcon_socket, &pkt)) {
74+
fprintf(stderr, "error: send command failed\n");
75+
return EXIT_FAILURE;
76+
}
77+
78+
if (rcon_recv(rcon_socket, &pkt, RCON_TYPE_RESPONSE) && pkt.length > 0) {
79+
puts(pkt.body);
80+
}
81+
82+
return EXIT_SUCCESS;
83+
}
84+
85+
char* combine_args(int argc, char* argv[]) {
86+
// combine all cli arguments
87+
char* command = malloc(MAX_BODY);
88+
memset(command, 0, MAX_BODY);
89+
strcat(command, argv[1]);
90+
91+
for (int idx = 2; idx < argc; idx++) {
92+
strcat(command, " ");
93+
strcat(command, argv[idx]);
94+
}
95+
96+
return command;
97+
}
98+
99+
char* read_password(const char* conf_dir) {
100+
char* path = malloc(strlen(conf_dir) + 64);
101+
strcpy(path, conf_dir);
102+
strcat(path, "/rconpw");
103+
104+
FILE* fptr = fopen(path, "r");
105+
fseek(fptr, 0, SEEK_END);
106+
long fsize = ftell(fptr);
107+
fseek(fptr, 0, SEEK_SET); /* same as rewind(f); */
108+
109+
char *password = malloc(fsize + 1);
110+
fread(password, fsize, 1, fptr);
111+
fclose(fptr);
112+
113+
password[fsize] = 0;
114+
if (password[fsize-1] == '\n') {
115+
password[fsize-1] = 0;
116+
}
117+
118+
return password;
119+
}
120+
121+
int rcon_open(const char *port) {
122+
struct sockaddr_in address = {
123+
.sin_family = AF_INET,
124+
.sin_port = htons(atoi(port))
125+
};
126+
inet_aton(RCON_HOST, &address.sin_addr);
127+
128+
int rcon_socket = socket(AF_INET, SOCK_STREAM, 0);
129+
if (connect(rcon_socket, (struct sockaddr*) &address, sizeof(address)) < 0) {
130+
return -1;
131+
} else {
132+
return rcon_socket;
133+
}
134+
}
135+
136+
void rcon_create(packet* pkt, packet_type type, const char* body) {
137+
size_t body_length = strlen(body);
138+
if (body_length >= MAX_BODY - 2) {
139+
fprintf(stderr, "error: command to long");
140+
exit(EXIT_FAILURE);
141+
}
142+
143+
pkt->id = abs(rand());
144+
pkt->type = type;
145+
pkt->length = (uint32_t)(sizeof(pkt->id) + sizeof(pkt->type) + body_length + 2);
146+
147+
memset(pkt->body, 0, MAX_BODY);
148+
strncpy(pkt->body, body, MAX_BODY);
149+
}
150+
151+
bool rcon_recv(int rcon_socket, packet* pkt, packet_type expected_type) {
152+
memset(pkt, 0, sizeof(*pkt));
153+
154+
// Read response packet length
155+
ssize_t expected_length_bytes = sizeof(pkt->length);
156+
ssize_t rx_bytes = recv(rcon_socket, &(pkt->length), expected_length_bytes, 0);
157+
158+
if (rx_bytes == -1) {
159+
perror("error: socket error");
160+
return false;
161+
} else if (rx_bytes == 0) {
162+
fprintf(stderr, "error: no data recieved\n");
163+
return false;
164+
} else if (rx_bytes < expected_length_bytes || pkt->length < MIN_PACKET || pkt->length > MAX_PACKET) {
165+
fprintf(stderr, "error: invalid data\n");
166+
return false;
167+
}
168+
169+
ssize_t received = 0;
170+
while (received < pkt->length) {
171+
rx_bytes = recv(rcon_socket, (char *)pkt + sizeof(pkt->length) + received, pkt->length - received, 0);
172+
if (rx_bytes < 0) {
173+
perror("error: socket error");
174+
return false;
175+
} else if (rx_bytes == 0) {
176+
fprintf(stderr, "error: connection lost\n");
177+
return false;
178+
}
179+
180+
received += rx_bytes;
181+
}
182+
183+
return pkt->type == expected_type;
184+
}
185+
186+
bool rcon_send(int rcon_socket, const packet* pkt) {
187+
size_t length = sizeof(pkt->length) + pkt->length;
188+
char *ptr = (char*) pkt;
189+
190+
while (length > 0) {
191+
ssize_t ret = send(rcon_socket, ptr, length, 0);
192+
193+
if (ret == -1) {
194+
return false;
195+
}
196+
197+
ptr += ret;
198+
length -= ret;
199+
}
200+
201+
return true;
202+
}
203+
204+
bool rcon_auth(int rcon_socket, const char* password) {
205+
packet pkt;
206+
rcon_create(&pkt, RCON_TYPE_AUTH, password);
207+
208+
if (!rcon_send(rcon_socket, &pkt)) {
209+
return false;
210+
}
211+
212+
if (!rcon_recv(rcon_socket, &pkt, RCON_TYPE_AUTH_RESPONSE)) {
213+
return false;
214+
}
215+
216+
return true;
217+
}

0 commit comments

Comments
 (0)