summaryrefslogtreecommitdiff
path: root/src/console
diff options
context:
space:
mode:
Diffstat (limited to 'src/console')
-rw-r--r--src/console/rcon.c238
-rw-r--r--src/console/rcon.h39
2 files changed, 277 insertions, 0 deletions
diff --git a/src/console/rcon.c b/src/console/rcon.c
new file mode 100644
index 0000000..5e958e1
--- /dev/null
+++ b/src/console/rcon.c
@@ -0,0 +1,238 @@
+#include "rcon.h"
+#include "../log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+
+/* ── Packet structure ────────────────────────────────────────────────────── */
+/*
+ * On the wire, each RCON packet is:
+ * [int32 size] [int32 id] [int32 type] [string\0] [\0]
+ *
+ * 'size' = sizeof(id) + sizeof(type) + strlen(body) + 2
+ * = 4 + 4 + strlen(body) + 2
+ *
+ * We use a fixed-size buffer — RCON responses for console commands are
+ * generally small (< 4KB), but we handle multi-packet responses.
+ */
+
+#define RCON_BUF_SIZE 4096
+#define RCON_HDR_SIZE 12 /* size(4) + id(4) + type(4) */
+#define RCON_TIMEOUT_S 5
+
+typedef struct {
+ int32_t size;
+ int32_t id;
+ int32_t type;
+ char body[RCON_BUF_SIZE];
+} RconPacket;
+
+/* ── Low-level helpers ───────────────────────────────────────────────────── */
+
+static int tcp_connect(const char *host, int port) {
+ struct addrinfo hints, *res, *rp;
+ char port_str[8];
+ int fd = -1;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ snprintf(port_str, sizeof(port_str), "%d", port);
+ if (getaddrinfo(host, port_str, &hints, &res) != 0) {
+ log_error("rcon: getaddrinfo(%s): %s", host, strerror(errno));
+ return -1;
+ }
+
+ for (rp = res; rp; rp = rp->ai_next) {
+ fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (fd < 0) continue;
+
+ /* Set connect timeout */
+ struct timeval tv = { .tv_sec = RCON_TIMEOUT_S, .tv_usec = 0 };
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+
+ if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0)
+ break;
+
+ close(fd);
+ fd = -1;
+ }
+
+ freeaddrinfo(res);
+ if (fd < 0)
+ log_error("rcon: could not connect to %s:%d", host, port);
+ return fd;
+}
+
+static int rcon_send(int fd, int32_t id, int32_t type, const char *body) {
+ int body_len = body ? (int)strlen(body) : 0;
+ int32_t size = 4 + 4 + body_len + 2; /* id + type + body + two nulls */
+
+ /* Build packet in a local buffer */
+ uint8_t buf[RCON_BUF_SIZE + RCON_HDR_SIZE];
+ int32_t le_size = htole32(size);
+ int32_t le_id = htole32(id);
+ int32_t le_type = htole32(type);
+
+ memcpy(buf, &le_size, 4);
+ memcpy(buf + 4, &le_id, 4);
+ memcpy(buf + 8, &le_type, 4);
+ if (body_len > 0)
+ memcpy(buf + 12, body, body_len);
+ buf[12 + body_len] = '\0';
+ buf[12 + body_len + 1] = '\0';
+
+ int total = 4 + size;
+ ssize_t n = write(fd, buf, total);
+ return (n == total) ? 0 : -1;
+}
+
+static int rcon_recv(int fd, RconPacket *pkt) {
+ uint8_t hdr[12];
+ ssize_t n;
+
+ /* Read the fixed 12-byte header */
+ int got = 0;
+ while (got < 12) {
+ n = read(fd, hdr + got, 12 - got);
+ if (n <= 0) return -1;
+ got += n;
+ }
+
+ int32_t size, id, type;
+ memcpy(&size, hdr, 4); size = le32toh(size);
+ memcpy(&id, hdr + 4, 4); id = le32toh(id);
+ memcpy(&type, hdr + 8, 4); type = le32toh(type);
+
+ pkt->size = size;
+ pkt->id = id;
+ pkt->type = type;
+
+ /* Body length = size - sizeof(id) - sizeof(type) - 2 trailing nulls */
+ int body_len = size - 4 - 4 - 2;
+ if (body_len < 0 || body_len >= RCON_BUF_SIZE)
+ return -1;
+
+ if (body_len > 0) {
+ got = 0;
+ while (got < body_len) {
+ n = read(fd, pkt->body + got, body_len - got);
+ if (n <= 0) return -1;
+ got += n;
+ }
+ }
+ pkt->body[body_len] = '\0';
+
+ /* Consume the two trailing null bytes */
+ uint8_t nulls[2];
+ (void)read(fd, nulls, 2);
+
+ return 0;
+}
+
+/* ── Public interface ────────────────────────────────────────────────────── */
+
+int rcon_exec(const char *host, int port, const char *password,
+ const char *command, char *response, int resp_len) {
+ int fd = tcp_connect(host, port);
+ if (fd < 0) return -1;
+
+ RconPacket pkt;
+ int auth_id = 1;
+ int exec_id = 2;
+
+ /* Step 1: Send auth request */
+ if (rcon_send(fd, auth_id, RCON_AUTH_REQUEST, password) != 0) {
+ log_error("rcon: send auth failed");
+ close(fd);
+ return -1;
+ }
+
+ /*
+ * Step 2: Read auth response.
+ *
+ * srcds sends packets in this order after an auth request:
+ * 1. SERVERDATA_RESPONSE_VALUE (type=0, id=auth_id) -- empty body
+ * 2. SERVERDATA_AUTH_RESPONSE (type=2, id=auth_id) -- success
+ * OR
+ * SERVERDATA_AUTH_RESPONSE (type=2, id=-1) -- bad password
+ *
+ * Loop ignoring type=0 packets until we see type=2.
+ * 8 attempts handles servers that send extra interleaved packets.
+ */
+ int authed = 0;
+ for (int attempt = 0; attempt < 8; attempt++) {
+ if (rcon_recv(fd, &pkt) != 0) {
+ log_error("rcon: recv auth response failed (attempt %d)", attempt);
+ close(fd);
+ return -1;
+ }
+
+ log_debug("rcon: auth packet type=%d id=%d", pkt.type, pkt.id);
+
+ if (pkt.type == 2) {
+ if (pkt.id == -1) {
+ log_error("rcon: bad password -- verify rcon_password in "
+ "server.cfg matches the unit file configuration");
+ close(fd);
+ return -1;
+ }
+ authed = 1;
+ break;
+ }
+ /* type=0: empty value packet, keep reading */
+ }
+
+ if (!authed) {
+ log_error("rcon: no auth response after 8 packets");
+ close(fd);
+ return -1;
+ }
+
+ /* Step 3: Send command */
+ if (rcon_send(fd, exec_id, RCON_EXEC_REQUEST, command) != 0) {
+ log_error("rcon: send command failed");
+ close(fd);
+ return -1;
+ }
+
+ /*
+ * Step 4: Read response.
+ * Large responses may be split across multiple packets.
+ * We send a known "terminator" packet and collect until we see its id.
+ * This is the standard trick for handling multi-packet responses.
+ */
+ int term_id = 3;
+ rcon_send(fd, term_id, RCON_EXEC_REQUEST, "");
+
+ int pos = 0;
+ if (response) response[0] = '\0';
+
+ for (int i = 0; i < 64; i++) { /* safety limit */
+ if (rcon_recv(fd, &pkt) != 0) break;
+
+ if (pkt.id == term_id)
+ break; /* saw our terminator response — done */
+
+ if (response && pkt.body[0]) {
+ int chunk = (int)strlen(pkt.body);
+ if (pos + chunk < resp_len - 1) {
+ memcpy(response + pos, pkt.body, chunk);
+ pos += chunk;
+ response[pos] = '\0';
+ }
+ }
+ }
+
+ close(fd);
+ return 0;
+}
diff --git a/src/console/rcon.h b/src/console/rcon.h
new file mode 100644
index 0000000..ffcbde7
--- /dev/null
+++ b/src/console/rcon.h
@@ -0,0 +1,39 @@
+#ifndef UMBRELLA_RCON_H
+#define UMBRELLA_RCON_H
+
+/*
+ * Valve RCON protocol implementation.
+ * https://developer.valvesoftware.com/wiki/Source_RCON_Protocol
+ *
+ * RCON uses a simple TCP framing:
+ * [int32 size][int32 id][int32 type][string body][null][null]
+ *
+ * Types:
+ * SERVERDATA_AUTH = 3
+ * SERVERDATA_AUTH_RESPONSE = 2
+ * SERVERDATA_EXECCOMMAND = 2 (same value as AUTH_RESPONSE — context-dependent)
+ * SERVERDATA_RESPONSE_VALUE = 0
+ */
+
+#define RCON_AUTH_REQUEST 3
+#define RCON_AUTH_RESPONSE 2
+#define RCON_EXEC_REQUEST 2
+#define RCON_EXEC_RESPONSE 0
+
+/*
+ * rcon_exec: connect to an RCON server, authenticate, run a command,
+ * collect the response, and disconnect.
+ *
+ * host - server hostname or IP
+ * port - RCON port (usually 27015)
+ * password - RCON password
+ * command - command string to execute
+ * response - output buffer
+ * resp_len - size of response buffer
+ *
+ * Returns 0 on success, -1 on failure.
+ */
+int rcon_exec(const char *host, int port, const char *password,
+ const char *command, char *response, int resp_len);
+
+#endif /* UMBRELLA_RCON_H */