#include "rcon.h" #include "../log.h" #include #include #include #include #include #include #include #include #include /* ── 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; }