diff options
Diffstat (limited to 'src/console')
| -rw-r--r-- | src/console/rcon.c | 238 | ||||
| -rw-r--r-- | src/console/rcon.h | 39 |
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 */ |
