Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/src/content/docs/setup/plugin-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions game/addons/sourcemod/scripting/include/sbpp_server_ip.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
23 changes: 16 additions & 7 deletions game/addons/sourcemod/scripting/sbpp_checker.sp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

#include <sourcemod>
#include <sbpp_version>
#include <sbpp_server_ip>

#define VERSION SB_VERSION
#define LISTBANS_USAGE "sm_listbans <#userid|name> - Lists a user's prior bans from Sourcebans"
Expand Down Expand Up @@ -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]
);
Expand Down Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion game/addons/sourcemod/scripting/sbpp_comms.sp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
39 changes: 31 additions & 8 deletions game/addons/sourcemod/scripting/sbpp_main.sp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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]);
Comment thread
rumblefrog marked this conversation as resolved.
}
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]);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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')
Comment thread
rumblefrog marked this conversation as resolved.
{
char sQuery[256];

Expand All @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion game/addons/sourcemod/scripting/sbpp_sleuth.sp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <sourcemod>
#undef REQUIRE_PLUGIN
#include <sourcebanspp>
#include <sbpp_server_ip>

#define PLUGIN_VERSION "1.8.0"

Expand Down Expand Up @@ -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));

Expand Down
5 changes: 5 additions & 0 deletions game/addons/sourcemod/translations/sbpp_main.phrases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
4 changes: 2 additions & 2 deletions web/tests/integration/UpdaterMigrationPortableSqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 92 additions & 0 deletions web/updater/data/811.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

// Steam Networking (SDR / FakeIP) cleanup. Valve's SDK 2013 hands the server a
// placeholder 169.254.x.x address for SDR clients instead of a real IP. It is
// shared by every such player, so it identifies nobody; the SourceMod plugins
// no longer store it (see `SBPP_GetClientPublicIP` in `sbpp_server_ip.inc`).
// This migration removes the placeholder from rows written by older plugins.
//
// 1. IP bans (`type = 1`) of an SDR player: the "IP" was the shared
// placeholder, so the row blocked (or, worse, would block) every SDR
// client. The plugin's SteamID-only check can't see `type = 1` rows, so
// convert the row to a SteamID ban (`type = 0`) when it carries a valid
// SteamID, and clear the placeholder. Rows without a valid SteamID are
// left as they are, only the placeholder is cleared below.
// 2. Any other `ip` still holding a placeholder is set to NULL, matching
// the shape of SteamID bans written without an IP.
// 3. `adminIp` placeholders (admins connecting over SDR) are blanked in both
// `:prefix_bans` and `:prefix_comms`.
// 4. In-game reports (`SBReportPlayer`) store the reporter's IP in
// `:prefix_submissions.ip` and the target's in `sip`. Placeholders there
// are blanked (`ip` is NOT NULL) or set to NULL (`sip`).
//
// `:prefix_protests.ip` is NOT touched: it holds the web visitor's address,
// not a game client's.
//
// Every pass is idempotent: once a row has been rewritten it no longer matches
// its own WHERE clause, so a re-run after a partial failure is safe.
//
// `$this` is supplied by Updater::update(); PHPStan can't see that, so each
// `$this->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;
3 changes: 2 additions & 1 deletion web/updater/store.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@
"807": "807.php",
"808": "808.php",
"809": "809.php",
"810": "810.php"
"810": "810.php",
"811": "811.php"
}
Loading