// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by AJ "Tyron" Martinez // Copyright (C) 2024 by Kart Krew // Copyright (C) 2020 by Sonic Team Junior // Copyright (C) 2000 by DooM Legacy Team // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file k_bans.c /// \brief replacement for DooM Legacy ban system #include #include #include #include #include #include "i_tcp_detail.h" // clientaddress #include "k_bans.h" #include "byteptr.h" // READ/WRITE macros #include "command.h" #include "d_clisrv.h" // playernode #include "d_main.h" // pandf #include "d_net.h" // MAXNETNODES #include "doomtype.h" #include "k_profiles.h" #include "m_misc.h" //FIL_WriteFile() #include "p_saveg.h" // savebuffer_t #include "time.h" #include "z_zone.h" #include "i_addrinfo.h" // I_getaddrinfo using nlohmann::json; static mysockaddr_t *DuplicateSockAddr(const mysockaddr_t *source) { auto dup = static_cast(Z_Malloc(sizeof *source, PU_STATIC, NULL)); memcpy(dup, source, sizeof *source); return dup; } static std::vector bans; static uint8_t allZero[PUBKEYLENGTH]; static void load_bans_array_v1(json& array) { for (json& object : array) { uint8_t public_key_bin[PUBKEYLENGTH]; std::string public_key = object.at("public_key"); std::string ip_address = object.at("ip_address"); time_t expires = object.at("expires"); UINT8 subnet_mask = object.at("subnet_mask"); std::string username = object.at("username"); std::string reason = object.at("reason"); if (!FromPrettyRRID(public_key_bin, public_key.c_str())) { throw std::runtime_error("public_key has invalid format"); } bool banned = SV_BanIP( ip_address.c_str(), subnet_mask, public_key_bin, expires, username.c_str(), reason.c_str() ); if (!banned) { throw std::runtime_error(fmt::format("{}: IP address is invalid", ip_address)); } } } void SV_LoadBans(void) { if (!server) return; json object; try { std::ifstream f(va(pandf, srb2home, BANFILE)); if (f.is_open()) { f >> object; } } catch (const std::exception& ex) { const char* gdfolder = "the Ring Racers folder"; if (strcmp(srb2home, ".")) gdfolder = srb2home; I_Error("%s\nNot a valid ban file.\nDelete %s (maybe in %s) and try again.", ex.what(), BANFILE, gdfolder); } if (!object.is_object()) { return; } try { if (object.value("version", 1) == 1) { json& array = object.at("bans"); if (array.is_array()) { load_bans_array_v1(array); } } } catch (const std::exception& ex) { I_Error("%s: %s", BANFILE, ex.what()); } } void SV_SaveBans(void) { json object = json::object(); object["version"] = 1; json& array = object["bans"]; array = json::array(); for (banrecord_t& ban : bans) { if (ban.deleted) continue; array.push_back({ {"public_key", GetPrettyRRID(ban.public_key, false)}, {"ip_address", SOCK_AddrToStr(ban.address)}, {"subnet_mask", ban.mask}, {"expires", ban.expires}, {"username", ban.username}, {"reason", ban.reason}, }); } try { std::ofstream(va(pandf, srb2home, BANFILE)) << object; } catch (const std::exception& ex) { I_Error("%s\nCouldn't save bans. Are you out of disk space / playing in a protected folder?", ex.what()); } } mysockaddr_t convertedaddress; static mysockaddr_t* SV_NodeToBanAddress(UINT8 node) { convertedaddress = SOCK_DirectNodeToAddr(node); if (convertedaddress.any.sa_family == AF_INET) { convertedaddress.ip4.sin_port = 0; // mask was 32? } #ifdef HAVE_IPV6 else if (convertedaddress.any.sa_family == AF_INET6) { convertedaddress.ip6.sin6_port = 0; // mask was 128? } #endif return &convertedaddress; } static boolean SV_IsBanEnforced(banrecord_t *ban) { if (ban->deleted) return false; if ((ban->expires != 0) && (time(NULL) > ban->expires)) return false; return true; } banrecord_t* SV_GetBanByAddress(UINT8 node) { mysockaddr_t* address = SV_NodeToBanAddress(node); for (banrecord_t& ban : bans) { if (!SV_IsBanEnforced(&ban)) continue; if (SOCK_cmpaddr(ban.address, address, ban.mask)) return &ban; } return NULL; } banrecord_t* SV_GetBanByKey(uint8_t* key) { UINT32 hash; hash = quickncasehash((char*) key, PUBKEYLENGTH); for (banrecord_t& ban : bans) { if (!SV_IsBanEnforced(&ban)) continue; if (hash != ban.hash) // Not crypto magic, just an early out with a faster comparison continue; if (memcmp(&ban.public_key, allZero, PUBKEYLENGTH) == 0) continue; // Don't ban GUESTs on accident, we have a cvar for this. if (memcmp(&ban.public_key, key, PUBKEYLENGTH) == 0) return &ban; } return NULL; } void SV_BanPlayer(INT32 pnum, time_t minutes, char* reason) { mysockaddr_t targetaddress; UINT8 node = playernode[pnum]; time_t expires = 0; memcpy(&targetaddress, SV_NodeToBanAddress(node), sizeof(mysockaddr_t)); if (minutes != 0) expires = time(NULL) + minutes * 60; SV_Ban(targetaddress, 0, players[pnum].public_key, expires, player_names[pnum], reason); } boolean SV_BanIP(const char *address, UINT8 mask, uint8_t* public_key, time_t expires, const char* username, const char* reason) { struct my_addrinfo *ai, *runp, hints; memset(&hints, 0x00, sizeof(hints)); hints.ai_flags = 0; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; int gaie; gaie = I_getaddrinfo(address, "0", &hints, &ai); if (gaie != 0) return false; runp = ai; mysockaddr_t newaddr; memcpy(&newaddr, runp->ai_addr, runp->ai_addrlen); SV_Ban(newaddr, mask, public_key, expires, username, reason); return true; } static char noreason[] = "N/A"; static char nouser[] = "[Direct IP ban]"; void SV_Ban(mysockaddr_t address, UINT8 mask, uint8_t* public_key, time_t expires, const char* username, const char* reason) { if (reason == NULL || strlen(reason) == 0) reason = noreason; if (!public_key) public_key = allZero; if (!expires) expires = 0; if (!username) username = nouser; banrecord_t ban{}; ban.address = DuplicateSockAddr(&address); memcpy(ban.public_key, public_key, PUBKEYLENGTH); memcpy(&ban.expires, &expires, sizeof(time_t)); ban.mask = mask; strlcpy(ban.username, username, MAXBANUSERNAME); strlcpy(ban.reason, reason, MAXBANREASON); ban.hash = quickncasehash((char*) ban.public_key, PUBKEYLENGTH); bans.push_back(ban); SV_SaveBans(); } static void SV_BanSearch(boolean remove) { const char* filtertarget = (COM_Argc() > 1) ? COM_Argv(1) : NULL; if (remove && !filtertarget) { CONS_Printf("unban : removes all bans matching target username, key, or address\n"); return; } time_t now = time(NULL); UINT32 records = 0; UINT32 matchedrecords = 0; boolean force = COM_CheckPartialParm("-f"); // First pass: What records are we even looking for? for (banrecord_t& ban : bans) { ban.matchesquery = false; if (ban.deleted) continue; records++; const char* stringaddress = SOCK_AddrToStr(ban.address); const char* stringkey = GetPrettyRRID(ban.public_key, true); if (filtertarget != NULL && !(strcasestr(filtertarget, ban.username) || strcasestr(filtertarget, stringkey) || strcasestr(filtertarget, stringaddress))) continue; ban.matchesquery = true; matchedrecords++; } boolean saferemove = remove && (matchedrecords <= 1 || force); // Second pass: Report and/or act on records. for (banrecord_t& ban : bans) { if (!ban.matchesquery) continue; const char* stringaddress = SOCK_AddrToStr(ban.address); const char* stringkey = GetPrettyRRID(ban.public_key, true); std::string recordprint = fmt::format( "{}{} - {} [{}] - {}", stringaddress, ban.mask && ban.mask != 32 ? fmt::format("/{}", ban.mask) : "", ban.username, stringkey, ban.reason ); if (ban.expires) { if (ban.expires < now) recordprint += " - EXPIRED"; else recordprint += fmt::format(" - expires {}m", (ban.expires - now)/60); } CONS_Printf("%s\n", recordprint.c_str()); if (saferemove) ban.deleted = true; } if (records == 0) CONS_Printf("You haven't banned anyone yet.\n"); else if (matchedrecords == 0) CONS_Printf("\x82""No matches in %d bans.\n", records); else if (remove && !saferemove) CONS_Printf("\x82""Didn't unban, would remove %d bans.\x80 Refine search or use 'unban -f'.\n", matchedrecords); else if (saferemove) CONS_Printf("\x83""Removed %d/%d bans.\n", matchedrecords, records); else if (filtertarget) CONS_Printf("Matched %d/%d bans.\n", matchedrecords, records); else CONS_Printf("Showing %d bans. Try 'listbans [search]' to refine results.\n", matchedrecords); if (saferemove) SV_SaveBans(); } void Command_Listbans(void) { if (netgame && consoleplayer != serverplayer) { CONS_Printf("You must be the netgame host to do this.\n"); return; } SV_BanSearch(false); } void Command_Unban(void) { if (netgame && consoleplayer != serverplayer) { CONS_Printf("You must be the netgame host to do this.\n"); return; } SV_BanSearch(true); } void Command_BanIP(void) { if (COM_Argc() <= 1) { CONS_Printf("banip [reason]: bans the specified IP from netgames you host\n"); return; } char* input = Z_StrDup(COM_Argv(1)); const char *address = strtok(input, "/"); const char *mask = strtok(NULL, ""); if (!mask) mask = ""; char reason[MAXBANREASON+1] = ""; if (COM_Argc() > 2) strncpy(reason, COM_Argv(2), MAXBANREASON); UINT8 nummask = atoi(mask); if (SV_BanIP(address, nummask, NULL, 0, NULL, reason)) { CONS_Printf("Banned address %s.\n", address); } else { CONS_Printf("Not a valid IP address.\n"); } Z_Free(input); }