diff --git a/CHANGELOG.md b/CHANGELOG.md index fe000f28d..fbd43d7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ Legend: ? = Other stuff ``` +(26/10/26): Version 2.2.5-srcdslab (**Run updater required**) +----------------------- +``` +01. ! Players who joined before the database connected, hit a failed + ban query, or were online when sbpp_main late-loaded were never + checked for bans; the retry timer now re-queries +02. ! Steam Networking (SDR / FakeIP) players: the shared 169.254.x.x + placeholder is no longer stored or matched as their IP; sm_banip + refuses them, SourceSleuth skips them, and updater migration + 816 cleans placeholders stored by older plugins +03. ! SourceSleuth no longer re-bans a player on their own ban (with + sm_sleuth_actions 3 the length doubled on every reconnect) +``` + (26/10/26): Version 2.2.4-srcdslab ----------------------- ``` diff --git a/docs/src/content/docs/setup/plugin-setup.md b/docs/src/content/docs/setup/plugin-setup.md index 862898033..be547fcd1 100644 --- a/docs/src/content/docs/setup/plugin-setup.md +++ b/docs/src/content/docs/setup/plugin-setup.md @@ -85,6 +85,30 @@ Drop the plugins you want into `addons/sourcemod/plugins/` and remove the ones you don't (SourceMod loads everything it finds, so an unused plugin still consumes a server slot). +### Steam Networking (SDR / FakeIP) servers + +Servers using `-enablefakeip` or `sv_use_steam_networking` (Source SDK +2013 games) see every Steam Networking client under a placeholder +`169.254.x.x` address instead of the player's public IP. That address +is shared by every such player and identifies nobody, so the plugins +never store or match it: + +- **Bans and comms** record no IP for these players (neither the + target's nor admins). Bans are applied by SteamID. +- **`sm_banip`** refuses to ban a player who has no usable IP; use + `sm_ban` instead. +- **`sbpp_checker.smx` and `sbpp_main.smx`** only look up these players + via SteamID when they connect. +- **`sbpp_sleuth.smx`** skips them entirely. Its IP-based alt-account + detection can't work without a public IP address, and matching the + shared placeholder IP would flag innocent players. + +Players on the same server who connect normally, without SDR, are +checked exactly as before. The panel updater (migration 816) removes +placeholder addresses already stored by older plugins: SDR IP bans that +have a valid SteamID are converted to SteamID bans, and placeholder +IPs are cleared. + ### Discord notifications For a Discord-channel forwarder, install the separate diff --git a/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc b/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc new file mode 100644 index 000000000..69f85feab --- /dev/null +++ b/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc @@ -0,0 +1,67 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2026 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// ************************************************************************* + +#if defined _sbpp_server_ip_included +#endinput +#endif +#define _sbpp_server_ip_included + +/** + * SDR / Steam Networking ("FakeIP") clients are not reachable by a real address. + * The engine hands the server a placeholder instead. From the Source SDK 2013 + * Steamworks headers (steamnetworkingtypes.h): + * + * FakeIP addresses are designed to (hopefully) pass through existing code as + * transparently as possible, while conflicting with "real" addresses that might + * be in use on networks (both the Internet and LANs) in the same code as little + * as possible. At the time this comment is being written, they come from the + * 169.254.0.0/16 range, and the port number will always be >1024. HOWEVER, + * this is subject to change! Do not make assumptions about these addresses, + * or your code might break in the future. In particular, you should use + * functions such as ISteamNetworkingUtils::IsFakeIP to determine if an IP + * address is a "fake" one used by this system. + * + * https://github.com/ValveSoftware/source-sdk-2013/blob/be57e3d01ac7d7aa6a2cacbfef187c0b46616db0/src/public/steam/isteamnetworkingsockets.h#L812-L820 + * + * + * ISteamNetworkingUtils::IsFakeIP is a C++ interface that SourceMod does not + * expose to SourcePawn, so SourceBans++ falls back to the 169.254.0.0/16 range + * described above. If a native for IsFakeIP ever becomes available, this is + * the single place to switch to it. + * + * @param ip Client IP address string (without port). + * @return True if the address is a Steam Networking FakeIP. + */ +stock bool SBPP_IsSteamNetworkingIP(const char[] ip) +{ + return strncmp(ip, "169.254.", 8) == 0; +} + +/** + * GetClientIP wrapper that never returns a Steam Networking FakeIP, so callers + * can't store or match it by accident. A FakeIP is shared by every SDR client + * and says nothing about the player. + * + * @param client Client index. + * @param buffer Buffer for the IP (port removed). Emptied when unavailable. + * @param maxlen Size of buffer. + * @return True if buffer holds a usable IP, false if it is empty + * (lookup failed or the address is a FakeIP). + */ +stock bool SBPP_GetClientPublicIP(int client, char[] buffer, int maxlen) +{ + if (!GetClientIP(client, buffer, maxlen, true) || SBPP_IsSteamNetworkingIP(buffer)) + { + buffer[0] = '\0'; + return false; + } + + return true; +} diff --git a/game/addons/sourcemod/scripting/sbpp_checker.sp b/game/addons/sourcemod/scripting/sbpp_checker.sp index 2b066b5b0..30f69288c 100644 --- a/game/addons/sourcemod/scripting/sbpp_checker.sp +++ b/game/addons/sourcemod/scripting/sbpp_checker.sp @@ -29,6 +29,7 @@ #include #include +#include #define VERSION SB_VERSION #define LISTBANS_USAGE "sm_listbans <#userid|name> - Lists a user's prior bans from Sourcebans" @@ -201,18 +202,23 @@ public void OnClientAuthorized(int client, const char[] auth) if (auth[0] == 'B' || auth[9] == 'L') return; - char query[512], ip[30]; - GetClientIP(client, ip, sizeof(ip)); + // No usable IP (e.g. SDR client): never match IP bans, an empty address + // would match any type=1 row with a blank IP. + char query[512], ip[30], ipCondition[64]; + if (SBPP_GetClientPublicIP(client, ip, sizeof(ip))) + FormatEx(ipCondition, sizeof(ipCondition), "ip = '%s'", ip); + else + strcopy(ipCondition, sizeof(ipCondition), "1 = 0"); FormatEx(query, sizeof(query), "SELECT COUNT(bid) FROM %s_bans WHERE (type = 0 AND authid LIKE 'STEAM_%%:%s') \ UNION ALL \ - SELECT COUNT(bid) FROM %s_bans WHERE (type = 1 AND ip = '%s') \ + SELECT COUNT(bid) FROM %s_bans WHERE (type = 1 AND %s) \ UNION ALL \ SELECT COUNT(bid) FROM %s_comms WHERE authid LIKE 'STEAM_%%:%s' AND type = 1 \ UNION ALL \ SELECT COUNT(bid) FROM %s_comms WHERE authid LIKE 'STEAM_%%:%s' AND type = 2", g_DatabasePrefix, auth[8], - g_DatabasePrefix, ip, + g_DatabasePrefix, ipCondition, g_DatabasePrefix, auth[8], g_DatabasePrefix, auth[8] ); @@ -304,9 +310,12 @@ public Action OnListSourceBansCmd(int client, int args) return Plugin_Handled; } - char query[1024], ip[30]; - GetClientIP(target, ip, sizeof(ip)); - FormatEx(query, sizeof(query), "SELECT created, %s_admins.user, ends, length, reason, RemoveType FROM %s_bans LEFT JOIN %s_admins ON %s_bans.aid = %s_admins.aid WHERE ((type = 0 AND %s_bans.authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND ip = '%s')) AND ((length > '0' AND ends > UNIX_TIMESTAMP()) OR RemoveType IS NOT NULL)", g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, auth[8], ip); + char query[1024], ip[30], ipCondition[64]; + if (SBPP_GetClientPublicIP(target, ip, sizeof(ip))) + FormatEx(ipCondition, sizeof(ipCondition), "ip = '%s'", ip); + else + strcopy(ipCondition, sizeof(ipCondition), "1 = 0"); + FormatEx(query, sizeof(query), "SELECT created, %s_admins.user, ends, length, reason, RemoveType FROM %s_bans LEFT JOIN %s_admins ON %s_bans.aid = %s_admins.aid WHERE ((type = 0 AND %s_bans.authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND %s)) AND ((length > '0' AND ends > UNIX_TIMESTAMP()) OR RemoveType IS NOT NULL)", g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, auth[8], ipCondition); char targetName[MAX_NAME_LENGTH]; GetClientName(target, targetName, sizeof(targetName)); diff --git a/game/addons/sourcemod/scripting/sbpp_comms.sp b/game/addons/sourcemod/scripting/sbpp_comms.sp index 094b512e4..bda525370 100644 --- a/game/addons/sourcemod/scripting/sbpp_comms.sp +++ b/game/addons/sourcemod/scripting/sbpp_comms.sp @@ -30,6 +30,7 @@ #include #include #include +#include #undef REQUIRE_PLUGIN #include @@ -297,7 +298,7 @@ public bool OnClientConnect(int client, char[] rejectmsg, int maxlen) FormatEx(g_sSteamIDs[client], sizeof(g_sSteamIDs[]), "%s", sSteamID); char sIP[16]; - GetClientIP(client, sIP, sizeof(sIP)); + SBPP_GetClientPublicIP(client, sIP, sizeof(sIP)); FormatEx(g_sPlayerIP[client], sizeof(g_sPlayerIP[]), "%s", sIP); FormatEx(g_sName[client], sizeof(g_sName[]), "%N", client); g_iUserIDs[client] = GetClientUserId(client); diff --git a/game/addons/sourcemod/scripting/sbpp_main.sp b/game/addons/sourcemod/scripting/sbpp_main.sp index 668615cef..7e4544e32 100644 --- a/game/addons/sourcemod/scripting/sbpp_main.sp +++ b/game/addons/sourcemod/scripting/sbpp_main.sp @@ -28,6 +28,7 @@ #include #include +#include #undef REQUIRE_PLUGIN #include @@ -99,7 +100,8 @@ bool , backupConfig = true , enableAdmins = true , ConfigLoaded = false /* sourcebans.cfg has been parsed at least once */ - , PlayerStatus[MAXPLAYERS + 1]; /* Player ban check status */ + , PlayerStatus[MAXPLAYERS + 1] /* Player ban check status */ + , PlayerCheckPending[MAXPLAYERS + 1]; /* Ban check query in flight */ int g_BanTarget[MAXPLAYERS + 1] = { -1, ... } @@ -309,6 +311,7 @@ public void OnClientDisconnect(int client) PlayerRecheck[client] = INVALID_HANDLE; } + PlayerCheckPending[client] = false; FormatEx(g_sSteamIDs[client], sizeof(g_sSteamIDs[]), "\0"); FormatEx(g_sPlayerIP[client], sizeof(g_sPlayerIP[]), "\0"); FormatEx(g_sName[client], sizeof(g_sName[]), "\0"); @@ -319,13 +322,14 @@ public void OnClientDisconnect(int client) public bool OnClientConnect(int client, char[] rejectmsg, int maxlen) { PlayerStatus[client] = false; + PlayerCheckPending[client] = false; return true; } public void OnClientConnected(int client) { char sIP[16], auth[MAX_AUTHID_LENGTH]; - GetClientIP(client, sIP, sizeof(sIP)); + SBPP_GetClientPublicIP(client, sIP, sizeof(sIP)); FormatEx(g_sPlayerIP[client], sizeof(g_sPlayerIP[]), "%s", sIP); GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)); @@ -341,7 +345,7 @@ public void OnClientConnected(int client) GetClientAuthId(client, AuthId_Steam2, g_sSteamIDs[client], sizeof(g_sSteamIDs[]), false); /* Do not check bots nor check player with lan steamid. */ - if (auth[0] == 'B' || auth[9] == 'L' || DB == INVALID_HANDLE) + if (auth[0] == 'B' || auth[9] == 'L') { PlayerStatus[client] = true; return; @@ -350,13 +354,46 @@ public void OnClientConnected(int client) public void OnClientAuthorized(int client, const char[] auth) { - if (PlayerStatus[client]) + CheckClientBan(client); +} + +// Queries the ban tables for a connected, authorized client. Safe to call +// repeatedly: it no-ops once the client passed a check, while a query is in +// flight or a retry is scheduled, and while the database is unavailable +// (GotDatabase re-runs it for everyone once the connection is up). +void CheckClientBan(int client) +{ + if (PlayerStatus[client] || PlayerCheckPending[client] || PlayerRecheck[client] != INVALID_HANDLE || DB == INVALID_HANDLE) + return; + + // The SteamID cached in OnClientConnected can still be STEAM_ID_PENDING. + char auth[MAX_AUTHID_LENGTH]; + if (!GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth))) + return; + + strcopy(g_sSteamIDs[client], sizeof(g_sSteamIDs[]), auth); + + if (auth[0] == 'B' || auth[9] == 'L') + { + PlayerStatus[client] = true; return; + } char Query[512]; - if (DB.Format(Query, sizeof(Query), "SELECT bid, ip FROM %!s_bans WHERE ((type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND ip = '%s')) AND (length = '0' OR ends > UNIX_TIMESTAMP()) AND RemoveType IS NULL", DatabasePrefix, g_sSteamIDs[client][8], g_sPlayerIP[client]) >= sizeof(Query) - 1) + int written; + if (g_sPlayerIP[client][0] == '\0') + { + /* No usable IP (e.g. SDR client): only check by SteamID. */ + written = DB.Format(Query, sizeof(Query), "SELECT bid, ip FROM %!s_bans WHERE (type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$') AND (length = '0' OR ends > UNIX_TIMESTAMP()) AND RemoveType IS NULL", DatabasePrefix, g_sSteamIDs[client][8]); + } + else { - LogError("OnClientAuthorized query truncated for %L", client); + written = DB.Format(Query, sizeof(Query), "SELECT bid, ip FROM %!s_bans WHERE ((type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND ip = '%s')) AND (length = '0' OR ends > UNIX_TIMESTAMP()) AND RemoveType IS NULL", DatabasePrefix, g_sSteamIDs[client][8], g_sPlayerIP[client]); + } + + if (written >= sizeof(Query) - 1) + { + LogError("CheckClientBan query truncated for %L", client); return; } @@ -364,7 +401,19 @@ public void OnClientAuthorized(int client, const char[] auth) LogToFile(logFile, "Checking ban for: %s", g_sSteamIDs[client]); #endif - DB.Query(VerifyBan, Query, g_iUserIDs[client], DBPrio_High); + PlayerCheckPending[client] = true; + DB.Query(VerifyBan, Query, GetClientUserId(client), DBPrio_High); +} + +void CheckConnectedClientBans() +{ + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientConnected(i) && IsClientAuthorized(i)) + { + CheckClientBan(i); + } + } } public void OnRebuildAdminCache(AdminCachePart part) @@ -462,7 +511,8 @@ public Action CommandBan(int client, int args) return Plugin_Handled; } - if (!PlayerStatus[target]) + // Without a database the check can't run; the ban goes to the SQLite queue instead. + if (!PlayerStatus[target] && DB != INVALID_HANDLE) { // The target has not been banned verify. It must be completed before you can ban anyone. ReplyToCommand(admin, "%s%t", Prefix, "Ban Not Verified"); @@ -565,6 +615,13 @@ public Action CommandBanIp(int client, int args) ReplyToCommand(client, "You do not have Perm Ban Permission"); return Plugin_Handled; } + if (g_sPlayerIP[target][0] == '\0') + { + // No usable IP (e.g. Steam Networking / SDR client): an IP ban can't work. + ReplyToCommand(client, "%s%t", Prefix, "Cannot IP Ban No IP", target); + return Plugin_Handled; + } + if (!client) { // setup dummy adminAuth and adminIp for server @@ -1178,6 +1235,8 @@ public void GotDatabase(Database db, const char[] error, any data) loadAdmins = false; } g_bConnecting = false; + + CheckConnectedClientBans(); } public void VerifyInsert(Database db, DBResultSet results, const char[] error, DataPack dataPack) @@ -1385,7 +1444,7 @@ public void InsertBanIpCallback(Database db, DBResultSet results, const char[] e if(!IsClientInGame(i) || IsFakeClient(i)) continue; - if(strcmp(targetIP, g_sPlayerIP[i], false) == 0) + if(targetIP[0] != '\0' && strcmp(targetIP, g_sPlayerIP[i], false) == 0) { target = i; break; @@ -1821,11 +1880,14 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int if (!client) return; + PlayerCheckPending[client] = false; + /* Failure happen. Do retry with delay */ if (results == null) { LogToFile(logFile, "Verify Ban Query Failed: %s", error); - PlayerRecheck[client] = CreateTimer(RetryTime, ClientRecheck, client); + if (PlayerRecheck[client] == INVALID_HANDLE) + PlayerRecheck[client] = CreateTimer(RetryTime, ClientRecheck, client); return; } @@ -1844,7 +1906,7 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int int iBid = results.FetchInt(0); results.FetchString(1, sIP, sizeof sIP); - if (StrEqual(sIP, "")) + if (StrEqual(sIP, "") && clientIp[0] != '\0') { char sQuery[256]; @@ -1859,12 +1921,22 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int } } + // Without a usable IP (e.g. SDR client) never match IP bans: an empty + // address would otherwise match any type=1 row with a blank IP. + // clientIp comes from GetClientIP (dotted digits), so the condition + // is inserted unescaped (%!s) below. + char ipCondition[64]; + if (clientIp[0] != '\0') + FormatEx(ipCondition, sizeof(ipCondition), "ip = '%s'", clientIp); + else + strcopy(ipCondition, sizeof(ipCondition), "1 = 0"); + if (serverID == -1) { if (db.Format(Query, sizeof(Query), "INSERT INTO %!s_banlog (sid ,time ,name ,bid) VALUES \ ((SELECT sid FROM %!s_servers WHERE ip = '%s' AND port = '%s' LIMIT 0,1), UNIX_TIMESTAMP(), '%s', \ - (SELECT bid FROM %!s_bans WHERE ((type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND ip = '%s')) AND RemoveType IS NULL LIMIT 0,1))", - DatabasePrefix, DatabasePrefix, ServerIp, ServerPort, g_sName[client], DatabasePrefix, clientAuth[8], clientIp) < sizeof(Query) - 1) + (SELECT bid FROM %!s_bans WHERE ((type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND %!s)) AND RemoveType IS NULL LIMIT 0,1))", + DatabasePrefix, DatabasePrefix, ServerIp, ServerPort, g_sName[client], DatabasePrefix, clientAuth[8], ipCondition) < sizeof(Query) - 1) { db.Query(ErrorCheckCallback, Query, client, DBPrio_High); } @@ -1877,8 +1949,8 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int { if (db.Format(Query, sizeof(Query), "INSERT INTO %!s_banlog (sid ,time ,name ,bid) VALUES \ (%d, UNIX_TIMESTAMP(), '%s', \ - (SELECT bid FROM %!s_bans WHERE ((type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND ip = '%s')) AND RemoveType IS NULL LIMIT 0,1))", - DatabasePrefix, serverID, g_sName[client], DatabasePrefix, clientAuth[8], clientIp) < sizeof(Query) - 1) + (SELECT bid FROM %!s_bans WHERE ((type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND %!s)) AND RemoveType IS NULL LIMIT 0,1))", + DatabasePrefix, serverID, g_sName[client], DatabasePrefix, clientAuth[8], ipCondition) < sizeof(Query) - 1) { db.Query(ErrorCheckCallback, Query, client, DBPrio_High); } @@ -2343,12 +2415,13 @@ public void OverridesDone(Database db, DBResultSet results, const char[] error, public Action ClientRecheck(Handle timer, any client) { - if (!PlayerStatus[client] && IsClientConnected(client)) + PlayerRecheck[client] = INVALID_HANDLE; + + if (IsClientConnected(client) && IsClientAuthorized(client)) { - OnClientConnected(client); + CheckClientBan(client); } - PlayerRecheck[client] = INVALID_HANDLE; return Plugin_Stop; } @@ -3363,14 +3436,14 @@ stock AdminFlag[] CreateFlagLetters() stock void AccountForLateLoading() { + // The database connects asynchronously, so the actual ban checks run from + // GotDatabase once it's up. for (int i = 1; i <= MaxClients; i++) { - if (IsClientConnected(i) && !IsFakeClient(i)) + if (IsClientConnected(i)) { PlayerStatus[i] = false; - } - if (IsClientInGame(i) && !IsFakeClient(i) && IsClientAuthorized(i)) - { + PlayerCheckPending[i] = false; OnClientConnected(i); } } diff --git a/game/addons/sourcemod/scripting/sbpp_sleuth.sp b/game/addons/sourcemod/scripting/sbpp_sleuth.sp index 1f07bfb4a..e718e5f69 100644 --- a/game/addons/sourcemod/scripting/sbpp_sleuth.sp +++ b/game/addons/sourcemod/scripting/sbpp_sleuth.sp @@ -30,6 +30,7 @@ #include #undef REQUIRE_PLUGIN #include +#include #define PLUGIN_VERSION "1.8.0" @@ -190,7 +191,12 @@ public void OnClientPostAdminCheck(int client) if (CanUseSourcebans && !IsFakeClient(client)) { char steamid[32]; - GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + /* Without a validated SteamID the player's own bans can't be told + * apart from an alt's. */ + if (!GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid))) + { + return; + } if (g_cVar_bypass.BoolValue && CheckCommandAccess(client, "sleuth_admin", ADMFLAG_BAN, false)) { @@ -200,13 +206,30 @@ public void OnClientPostAdminCheck(int client) if (g_hAllowedArray.FindString(steamid) == -1) { char IP[32], Prefix[64]; - GetClientIP(client, IP, sizeof(IP)); + /* SDR / Steam Networking clients have no usable IP (see + * SBPP_GetClientPublicIP). Skip them rather than matching a + * placeholder address shared by every such player. */ + if (!SBPP_GetClientPublicIP(client, IP, sizeof(IP))) + { + return; + } g_cVar_sbprefix.GetString(Prefix, sizeof(Prefix)); char query[1024]; - FormatEx(query, sizeof(query), "SELECT * FROM %s_bans WHERE ip='%s' AND RemoveType IS NULL AND (ends > %d OR ((1 = %d AND length = 0 AND ends > %d) OR (0 = %d AND length = 0)))", Prefix, IP, g_cVar_bantype.IntValue == 0 ? GetTime() : 0, g_cVar_excludeOld.IntValue, GetTime() - g_cVar_excludeTime.IntValue, g_cVar_excludeOld.IntValue); + /* Only other accounts' bans count as evidence of an alt, and a + * player who is already banned is left to sbpp_main. Matching the + * player's own ban would re-ban them on every reconnect. Oldest + * ban first so length-based actions don't compound off bans + * SourceSleuth issued itself. */ + FormatEx(query, sizeof(query), "SELECT length FROM %s_bans \ + WHERE ip = '%s' AND RemoveType IS NULL \ + AND authid NOT REGEXP '^STEAM_[0-9]:%s$' \ + AND (ends > %d OR ((1 = %d AND length = 0 AND ends > %d) OR (0 = %d AND length = 0))) \ + AND NOT EXISTS (SELECT 1 FROM %s_bans WHERE type = 0 AND authid REGEXP '^STEAM_[0-9]:%s$' AND (length = 0 OR ends > UNIX_TIMESTAMP()) AND RemoveType IS NULL) \ + ORDER BY created ASC, bid ASC", + Prefix, IP, steamid[8], g_cVar_bantype.IntValue == 0 ? GetTime() : 0, g_cVar_excludeOld.IntValue, GetTime() - g_cVar_excludeTime.IntValue, g_cVar_excludeOld.IntValue, Prefix, steamid[8]); DataPack datapack = new DataPack(); @@ -243,6 +266,11 @@ public void SQL_CheckHim(Database db, DBResultSet results, const char[] error, D return; } + if (!client) + { + return; + } + if (results.FetchRow()) { int TotalBans = results.RowCount; @@ -253,7 +281,7 @@ public void SQL_CheckHim(Database db, DBResultSet results, const char[] error, D { case LENGTH_ORIGINAL: { - int length = results.FetchInt(6); + int length = results.FetchInt(0); int time = length / 60; BanPlayer(client, time); @@ -265,7 +293,7 @@ public void SQL_CheckHim(Database db, DBResultSet results, const char[] error, D } case LENGTH_DOUBLE: { - int length = results.FetchInt(6); + int length = results.FetchInt(0); int time = 0; diff --git a/game/addons/sourcemod/translations/sbpp_main.phrases.txt b/game/addons/sourcemod/translations/sbpp_main.phrases.txt index c2907d916..3cdba22aa 100644 --- a/game/addons/sourcemod/translations/sbpp_main.phrases.txt +++ b/game/addons/sourcemod/translations/sbpp_main.phrases.txt @@ -333,6 +333,24 @@ "sv" "\"{1}\" lade till en bannlysning (minuter: {2}) (ID/IP: {3}) (anledning: {4})." "tr" "\"{1}\" yasak ekledi (dakika: {2}) (ID/IP: {3}) (sebep: {4})." } + "Cannot IP Ban No IP" + { + "#format" "{1:N}" + "en" "Cannot IP ban {1}: no IP address is available for this player (Steam Networking / SDR)." + "de" "{1} kann nicht per IP gebannt werden: für diesen Spieler ist keine IP-Adresse verfügbar (Steam Networking / SDR)." + "ru" "Невозможно забанить {1} по IP: у этого игрока нет доступного IP-адреса (Steam Networking / SDR)." + "fr" "Impossible de bannir {1} par IP : aucune adresse IP n'est disponible pour ce joueur (Steam Networking / SDR)." + "pt" "Não é possível banir {1} por IP: nenhum endereço IP está disponível para este jogador (Steam Networking / SDR)." + "chi" "无法按 IP 封禁 {1}:该玩家没有可用的 IP 地址 (Steam Networking / SDR)。" + "da" "Kan ikke IP-udelukke {1}: ingen IP-adresse er tilgængelig for denne spiller (Steam Networking / SDR)." + "es" "No se puede banear por IP a {1}: no hay ninguna dirección IP disponible para este jugador (Steam Networking / SDR)." + "hu" "{1} nem tiltható ki IP alapján: ehhez a játékoshoz nem érhető el IP-cím (Steam Networking / SDR)." + "nl" "Kan {1} niet op IP bannen: er is geen IP-adres beschikbaar voor deze speler (Steam Networking / SDR)." + "no" "Kan ikke IP-utestenge {1}: ingen IP-adresse er tilgjengelig for denne spilleren (Steam Networking / SDR)." + "pl" "Nie można zbanować {1} po IP: brak dostępnego adresu IP dla tego gracza (Steam Networking / SDR)." + "sv" "Kan inte IP-bannlysa {1}: ingen IP-adress finns tillgänglig för den här spelaren (Steam Networking / SDR)." + "tr" "{1} IP ile yasaklanamıyor: bu oyuncu için kullanılabilir bir IP adresi yok (Steam Networking / SDR)." + } "Ban Success Name" { "#format" "{1:s}" diff --git a/web/tests/integration/Updater816SteamNetworkingIpTest.php b/web/tests/integration/Updater816SteamNetworkingIpTest.php new file mode 100644 index 000000000..2be43757d --- /dev/null +++ b/web/tests/integration/Updater816SteamNetworkingIpTest.php @@ -0,0 +1,95 @@ +run(ROOT . 'updater/data/816.php'); + } + + private function insertBan(int $type, ?string $ip, string $authid, string $adminIp = ''): int + { + $pdo = Fixture::rawPdo(); + $stmt = $pdo->prepare( + 'INSERT INTO `' . DB_PREFIX . '_bans` (type, ip, authid, name, created, ends, length, reason, aid, adminIp)' + . " VALUES (?, ?, ?, 'p', 0, 0, 0, 'r', 0, ?)" + ); + $stmt->execute([$type, $ip, $authid, $adminIp]); + + return (int) $pdo->lastInsertId(); + } + + /** @return array */ + private function ban(int $bid): array + { + $stmt = Fixture::rawPdo()->prepare('SELECT type, ip, authid, adminIp FROM `' . DB_PREFIX . '_bans` WHERE bid = ?'); + $stmt->execute([$bid]); + + return (array) $stmt->fetch(\PDO::FETCH_ASSOC); + } + + public function testCleansPlaceholderAddressesAndIsIdempotent(): void + { + $sdrIpBan = $this->insertBan(1, '169.254.12.34', 'STEAM_0:1:1234'); + $sdrNoSteam = $this->insertBan(1, '169.254.12.35', ''); + $steamBan = $this->insertBan(0, '169.254.1.1', 'STEAM_0:0:42', '169.254.9.9'); + $realIpBan = $this->insertBan(1, '203.0.113.7', 'STEAM_0:0:7', '198.51.100.1'); + + $pdo = Fixture::rawPdo(); + $pdo->exec('INSERT INTO `' . DB_PREFIX . "_comms` (authid, name, created, ends, length, reason, aid, adminIp, type) VALUES ('STEAM_0:0:5', 'p', 0, 0, 0, 'r', 0, '169.254.3.3', 1)"); + $pdo->exec('INSERT INTO `' . DB_PREFIX . "_submissions` (submitted, ModID, SteamId, name, email, reason, ip, sip) VALUES (0, 0, 'STEAM_0:0:6', 'p', '', 'r', '169.254.4.4', '169.254.5.5')"); + + $this->assertTrue($this->runMigration(), 'First run should succeed.'); + + $this->assertSame(['type' => 0, 'ip' => null, 'authid' => 'STEAM_0:1:1234', 'adminIp' => ''], + $this->normalise($this->ban($sdrIpBan)), 'SDR IP ban with a SteamID becomes a SteamID ban.'); + $this->assertSame(['type' => 1, 'ip' => null, 'authid' => '', 'adminIp' => ''], + $this->normalise($this->ban($sdrNoSteam)), 'SDR IP ban without a SteamID only loses the placeholder.'); + $this->assertSame(['type' => 0, 'ip' => null, 'authid' => 'STEAM_0:0:42', 'adminIp' => ''], + $this->normalise($this->ban($steamBan)), 'Placeholder ip and adminIp are cleared.'); + $this->assertSame(['type' => 1, 'ip' => '203.0.113.7', 'authid' => 'STEAM_0:0:7', 'adminIp' => '198.51.100.1'], + $this->normalise($this->ban($realIpBan)), 'Real addresses are left alone.'); + + $this->assertSame('', (string) $pdo->query('SELECT adminIp FROM `' . DB_PREFIX . "_comms` WHERE authid = 'STEAM_0:0:5'")->fetchColumn()); + $sub = $pdo->query('SELECT ip, sip FROM `' . DB_PREFIX . "_submissions` WHERE SteamId = 'STEAM_0:0:6'")->fetch(\PDO::FETCH_ASSOC); + $this->assertSame(['ip' => '', 'sip' => null], $sub); + + $this->assertTrue($this->runMigration(), 'Second run must be a no-op.'); + $this->assertSame(['type' => 1, 'ip' => '203.0.113.7', 'authid' => 'STEAM_0:0:7', 'adminIp' => '198.51.100.1'], + $this->normalise($this->ban($realIpBan))); + } + + /** + * @param array $row + * @return array + */ + private function normalise(array $row): array + { + $row['type'] = (int) $row['type']; + + return $row; + } +} diff --git a/web/tests/integration/UpdaterMigrationPortableSqlTest.php b/web/tests/integration/UpdaterMigrationPortableSqlTest.php index c19452f95..efd184ddd 100644 --- a/web/tests/integration/UpdaterMigrationPortableSqlTest.php +++ b/web/tests/integration/UpdaterMigrationPortableSqlTest.php @@ -25,14 +25,14 @@ * * Scope (focused, per #1498) * -------------------------- - * Only migrations with version >= 800 (the 8xx v2 block, 801-815 today) + * Only migrations with version >= 800 (the 8xx v2 block, 801-816 today) * are scanned. Two reasons: * * - Every supported upgrade path the issue names (v2 rc5 -> rc6, * v1.8.x -> v2) starts at `config.version = 705`, so the updater only * runs 801+. 801 is the first migration past 705 and the actual * blocker. Future migrations are always numbered above the current - * max (816+), so a `>= 800` floor guards every migration that will + * max (817+), so a `>= 800` floor guards every migration that will * ever be added going forward. * - Ten pre-800 migrations carry the same MariaDB-only syntax * (1, 112, 150, 153, 160, 241, 291, 295, 351, 355). They only run for diff --git a/web/updater/data/816.php b/web/updater/data/816.php new file mode 100644 index 000000000..feaace0a7 --- /dev/null +++ b/web/updater/data/816.php @@ -0,0 +1,92 @@ +dbs` call is suppressed like every sibling migration. + +// Pass 1: SDR IP bans with a real SteamID become SteamID bans. +// @phpstan-ignore variable.undefined +$this->dbs->query( + "UPDATE `:prefix_bans`" + . " SET `type` = 0, `ip` = NULL" + . " WHERE `type` = 1" + . " AND `ip` LIKE '169.254.%'" + . " AND `authid` REGEXP '^STEAM_[0-9]:[0-9]:[0-9]+$'" +); +// @phpstan-ignore variable.undefined +$this->dbs->execute(); + +// Pass 2: clear any remaining placeholder IP. +// @phpstan-ignore variable.undefined +$this->dbs->query( + "UPDATE `:prefix_bans`" + . " SET `ip` = NULL" + . " WHERE `ip` LIKE '169.254.%'" +); +// @phpstan-ignore variable.undefined +$this->dbs->execute(); + +// Pass 3: placeholder admin IPs. +// @phpstan-ignore variable.undefined +$this->dbs->query( + "UPDATE `:prefix_bans`" + . " SET `adminIp` = ''" + . " WHERE `adminIp` LIKE '169.254.%'" +); +// @phpstan-ignore variable.undefined +$this->dbs->execute(); + +// @phpstan-ignore variable.undefined +$this->dbs->query( + "UPDATE `:prefix_comms`" + . " SET `adminIp` = ''" + . " WHERE `adminIp` LIKE '169.254.%'" +); +// @phpstan-ignore variable.undefined +$this->dbs->execute(); + +// Pass 4: placeholder IPs from in-game reports. +// @phpstan-ignore variable.undefined +$this->dbs->query( + "UPDATE `:prefix_submissions`" + . " SET `ip` = ''" + . " WHERE `ip` LIKE '169.254.%'" +); +// @phpstan-ignore variable.undefined +$this->dbs->execute(); + +// @phpstan-ignore variable.undefined +$this->dbs->query( + "UPDATE `:prefix_submissions`" + . " SET `sip` = NULL" + . " WHERE `sip` LIKE '169.254.%'" +); +// @phpstan-ignore variable.undefined +$this->dbs->execute(); + +return true; diff --git a/web/updater/store.json b/web/updater/store.json index 0b2598205..08de0bbae 100644 --- a/web/updater/store.json +++ b/web/updater/store.json @@ -53,5 +53,6 @@ "812": "812.php", "813": "813.php", "814": "814.php", - "815": "815.php" + "815": "815.php", + "816": "816.php" }