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; }