diff --git a/docs/src/content/docs/setup/plugin-setup.md b/docs/src/content/docs/setup/plugin-setup.md index 434b499f9..88c914571 100644 --- a/docs/src/content/docs/setup/plugin-setup.md +++ b/docs/src/content/docs/setup/plugin-setup.md @@ -97,6 +97,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 811) 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 index eb075e130..d189b8970 100644 --- a/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc +++ b/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc @@ -15,6 +15,59 @@ #define SBPP_SERVER_IP_LENGTH 16 +/** + * 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; +} + stock bool SBPP_IsValidServerIpOverride(const char[] ip) { if (ip[0] == '\0') diff --git a/game/addons/sourcemod/scripting/sbpp_checker.sp b/game/addons/sourcemod/scripting/sbpp_checker.sp index e06781f59..7e6cc0bf1 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" @@ -187,18 +188,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] ); @@ -280,9 +286,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 c239bc88c..6894e5908 100644 --- a/game/addons/sourcemod/scripting/sbpp_comms.sp +++ b/game/addons/sourcemod/scripting/sbpp_comms.sp @@ -297,7 +297,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 bafda1392..4ac5f20b0 100644 --- a/game/addons/sourcemod/scripting/sbpp_main.sp +++ b/game/addons/sourcemod/scripting/sbpp_main.sp @@ -331,7 +331,7 @@ public bool OnClientConnect(int client, char[] rejectmsg, int maxlen) 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)); @@ -360,7 +360,15 @@ public void OnClientAuthorized(int client, const char[] auth) return; char Query[256]; - FormatEx(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 (g_sPlayerIP[client][0] == '\0') + { + /* No usable IP (e.g. SDR client): only check by SteamID. */ + FormatEx(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 + { + FormatEx(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 defined DEBUG LogToFile(logFile, "Checking ban for: %s", g_sSteamIDs[client]); @@ -578,6 +586,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 @@ -1307,7 +1322,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; @@ -1717,7 +1732,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]; @@ -1729,19 +1744,27 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int DB.Escape(g_sName[client], Name, sizeof Name); + // 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. + char ipCondition[64]; + if (clientIp[0] != '\0') + FormatEx(ipCondition, sizeof(ipCondition), "ip = '%s'", clientIp); + else + strcopy(ipCondition, sizeof(ipCondition), "1 = 0"); + if (serverID == -1) { FormatEx(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, ServerIpEscaped, ServerPort, Name, DatabasePrefix, clientAuth[8], clientIp); + (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, ServerIpEscaped, ServerPort, Name, DatabasePrefix, clientAuth[8], ipCondition); } else { FormatEx(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, Name, DatabasePrefix, clientAuth[8], clientIp); + (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, Name, DatabasePrefix, clientAuth[8], ipCondition); } db.Query(ErrorCheckCallback, Query, client, DBPrio_High); diff --git a/game/addons/sourcemod/scripting/sbpp_sleuth.sp b/game/addons/sourcemod/scripting/sbpp_sleuth.sp index 1f07bfb4a..2a1637969 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" @@ -200,7 +201,13 @@ 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)); diff --git a/game/addons/sourcemod/translations/sbpp_main.phrases.txt b/game/addons/sourcemod/translations/sbpp_main.phrases.txt index 98797e94f..4f2e80f58 100644 --- a/game/addons/sourcemod/translations/sbpp_main.phrases.txt +++ b/game/addons/sourcemod/translations/sbpp_main.phrases.txt @@ -236,6 +236,11 @@ "pt" "\"{1}\" adicionou um banimento. (Minutos: {2}.) (ID/IP: {3}.) (Motivo: {4}.)" "chi" "\"{1}\" 已被封禁 ({2} 分钟) (ID/IP: {3}) (理由: {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)." + } "Ban Success Name" { "#format" "{1:s}" diff --git a/web/tests/integration/UpdaterMigrationPortableSqlTest.php b/web/tests/integration/UpdaterMigrationPortableSqlTest.php index 7bee7f025..878a678b2 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-810 today) + * Only migrations with version >= 800 (the 8xx v2 block, 801-811 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 (811+), so a `>= 800` floor guards every migration that will + * max (812+), 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/811.php b/web/updater/data/811.php new file mode 100644 index 000000000..feaace0a7 --- /dev/null +++ b/web/updater/data/811.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 10e643d19..0378c8f28 100644 --- a/web/updater/store.json +++ b/web/updater/store.json @@ -48,5 +48,6 @@ "807": "807.php", "808": "808.php", "809": "809.php", - "810": "810.php" + "810": "810.php", + "811": "811.php" }