diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs
index 3510a5422e..164cf421bd 100644
--- a/Content.Server.Database/Model.cs
+++ b/Content.Server.Database/Model.cs
@@ -693,6 +693,14 @@ namespace Content.Server.Database
/// Intended use is for users with shared connections. This should not be used as an alternative to .
///
IP = 1 << 1,
+
+ ///
+ /// Ban is an IP range that is only applied for first time joins.
+ ///
+ ///
+ /// Intended for use with residential IP ranges that are often used maliciously.
+ ///
+ BlacklistedRange = 1 << 2,
// @formatter:on
}
diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs
index cd03af7087..d91ef39198 100644
--- a/Content.Server/Database/ServerDbBase.cs
+++ b/Content.Server/Database/ServerDbBase.cs
@@ -625,6 +625,11 @@ namespace Content.Server.Database
return record == null ? null : MakePlayerRecord(record);
}
+ protected async Task PlayerRecordExists(DbGuard db, NetUserId userId)
+ {
+ return await db.DbContext.Player.AnyAsync(p => p.UserId == userId);
+ }
+
[return: NotNullIfNotNull(nameof(player))]
protected PlayerRecord? MakePlayerRecord(Player? player)
{
diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs
index fd4699fff4..f8eef1a554 100644
--- a/Content.Server/Database/ServerDbPostgres.cs
+++ b/Content.Server/Database/ServerDbPostgres.cs
@@ -78,7 +78,8 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var exempt = await GetBanExemptionCore(db, userId);
- var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt)
+ var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
+ var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer)
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
@@ -98,7 +99,8 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var exempt = await GetBanExemptionCore(db, userId);
- var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt);
+ var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId);
+ var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer);
var queryBans = await query.ToArrayAsync();
var bans = new List(queryBans.Length);
@@ -122,7 +124,8 @@ namespace Content.Server.Database
ImmutableArray? hwId,
DbGuardImpl db,
bool includeUnbanned,
- ServerBanExemptFlags? exemptFlags)
+ ServerBanExemptFlags? exemptFlags,
+ bool newPlayer)
{
DebugTools.Assert(!(address == null && userId == null && hwId == null));
@@ -141,7 +144,9 @@ namespace Content.Server.Database
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
- .Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address));
+ .Where(b => b.Address != null
+ && EF.Functions.ContainsOrEqual(b.Address.Value, address)
+ && !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer));
query = query == null ? newQ : query.Union(newQ);
}
@@ -167,6 +172,9 @@ namespace Content.Server.Database
if (exemptFlags is { } exempt)
{
+ if (exempt != ServerBanExemptFlags.None)
+ exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange
+
query = query.Where(b => (b.ExemptFlags & exempt) == 0);
}
diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs
index ffec90bb43..ce6f97a117 100644
--- a/Content.Server/Database/ServerDbSqlite.cs
+++ b/Content.Server/Database/ServerDbSqlite.cs
@@ -86,11 +86,13 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
+ var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
+
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false, exempt);
- return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt)) is { } foundBan
+ return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt, newPlayer)) is { } foundBan
? ConvertBan(foundBan)
: null;
}
@@ -103,12 +105,14 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
+ var newPlayer = !await db.SqliteDbContext.Player.AnyAsync(p => p.UserId == userId);
+
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt);
return queryBans
- .Where(b => BanMatches(b, address, userId, hwId, exempt))
+ .Where(b => BanMatches(b, address, userId, hwId, exempt, newPlayer))
.Select(ConvertBan)
.ToList()!;
}
@@ -137,10 +141,18 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray? hwId,
- ServerBanExemptFlags? exemptFlags)
+ ServerBanExemptFlags? exemptFlags,
+ bool newPlayer)
{
+ // Any flag to bypass BlacklistedRange bans.
+ var exemptFromBlacklistedRange = exemptFlags != null && exemptFlags.Value != ServerBanExemptFlags.None;
+
if (!exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)
- && address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
+ && address != null
+ && ban.Address is not null
+ && address.IsInSubnet(ban.Address.ToTuple().Value)
+ && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) ||
+ newPlayer && !exemptFromBlacklistedRange))
{
return true;
}