refactor: rename FileAllowlist to ManualAllowlist
"File" described the storage, which is the least interesting thing about it. The distinction from LoginAllowlist is provenance: one is declared by an operator, the other is earned by authenticating. Both are IP allowlists, so IpAllowlist would not have separated them either. "Manual" matches vocabulary already in the subsystem -- BanReasons.Manual is the operator-declared ban, and the docs already describe this list as "an operator said so". Also moves both files into Network/ManualAllowlist/, mirroring Network/LoginAllowlist/. They no longer belong under Blocklist/ now that the config is decoupled and BanExemptions is the consumer that cannot be served any other way. Namespace is unchanged, so no using directives move. Configuration/ip-allowlist.json keeps its name: it is named for the ip-allowlist*.txt files it governs, whose names the generator owns, and that grouping is what an operator browsing Configuration/ actually sees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8b5a1e0c9
commit
bd187bd252
11 changed files with 53 additions and 53 deletions
|
|
@ -28,15 +28,15 @@ public class BanExemptionsTests
|
|||
private static readonly IPAddress _listed = IPAddress.Parse("192.0.2.10");
|
||||
private static readonly IPAddress _unlisted = IPAddress.Parse("192.0.2.11");
|
||||
|
||||
private static void WithFileAllowlist(string contents) =>
|
||||
FileAllowlist.LoadForTesting(BlocklistSnapshot.Build(Encoding.ASCII.GetBytes(contents), out _, out _));
|
||||
private static void WithManualAllowlist(string contents) =>
|
||||
ManualAllowlist.LoadForTesting(BlocklistSnapshot.Build(Encoding.ASCII.GetBytes(contents), out _, out _));
|
||||
|
||||
private static void WithEmptyFileAllowlist() => FileAllowlist.LoadForTesting(BlocklistSnapshot.Empty);
|
||||
private static void WithEmptyManualAllowlist() => ManualAllowlist.LoadForTesting(BlocklistSnapshot.Empty);
|
||||
|
||||
[Fact]
|
||||
public void File_allowlist_exempts_behavioral_contributions()
|
||||
public void Manual_allowlist_exempts_behavioral_contributions()
|
||||
{
|
||||
WithFileAllowlist("192.0.2.10");
|
||||
WithManualAllowlist("192.0.2.10");
|
||||
|
||||
// Subtracting from the blocklist does nothing for behavioural detections, which never consult it.
|
||||
Assert.True(BanExemptions.IsExempt(_listed, BanReasons.ForeignProtocol, NeverCalled));
|
||||
|
|
@ -46,10 +46,10 @@ public class BanExemptionsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void File_allowlist_covers_cidr_entries()
|
||||
public void Manual_allowlist_covers_cidr_entries()
|
||||
{
|
||||
// Carve-outs are CIDRs, so a shared-CGNAT player is only covered if ranges work here.
|
||||
WithFileAllowlist("192.0.2.0/24");
|
||||
WithManualAllowlist("192.0.2.0/24");
|
||||
|
||||
Assert.True(BanExemptions.IsExempt(_listed, BanReasons.RateLimit, NeverCalled));
|
||||
Assert.True(BanExemptions.IsExempt(IPAddress.Parse("192.0.2.254"), BanReasons.RateLimit, NeverCalled));
|
||||
|
|
@ -57,9 +57,9 @@ public class BanExemptionsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Manual_bans_are_never_exempt_even_when_file_allowlisted()
|
||||
public void Manual_bans_are_never_exempt_even_when_allowlisted()
|
||||
{
|
||||
WithFileAllowlist("192.0.2.10");
|
||||
WithManualAllowlist("192.0.2.10");
|
||||
|
||||
// An explicit decision outranks the operator's own carve-out, and must not cost a strike.
|
||||
Assert.False(BanExemptions.IsExempt(_listed, BanReasons.Manual, NeverCalled));
|
||||
|
|
@ -68,16 +68,16 @@ public class BanExemptionsTests
|
|||
[Fact]
|
||||
public void Unopted_reasons_are_never_exempt()
|
||||
{
|
||||
WithFileAllowlist("192.0.2.10");
|
||||
WithManualAllowlist("192.0.2.10");
|
||||
|
||||
Assert.False(BanExemptions.IsExempt(_listed, BanReasons.Blocklist, NeverCalled));
|
||||
Assert.False(BanExemptions.IsExempt(_listed, "some-future-reason", NeverCalled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_allowlist_does_not_spend_the_earned_lists_strikes()
|
||||
public void Manual_allowlist_does_not_spend_the_earned_lists_strikes()
|
||||
{
|
||||
WithFileAllowlist("192.0.2.10");
|
||||
WithManualAllowlist("192.0.2.10");
|
||||
|
||||
// Unconditional, so the revocable list must not be consulted -- that would burn a strike.
|
||||
Assert.True(BanExemptions.IsExempt(_listed, BanReasons.RateLimit, NeverCalled));
|
||||
|
|
@ -86,7 +86,7 @@ public class BanExemptionsTests
|
|||
[Fact]
|
||||
public void Falls_through_to_the_login_allowlist_when_not_file_listed()
|
||||
{
|
||||
WithEmptyFileAllowlist();
|
||||
WithEmptyManualAllowlist();
|
||||
|
||||
var consulted = 0;
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ public class BanExemptionsTests
|
|||
[Fact]
|
||||
public void Null_address_is_never_exempt()
|
||||
{
|
||||
WithEmptyFileAllowlist();
|
||||
WithEmptyManualAllowlist();
|
||||
|
||||
Assert.False(BanExemptions.IsExempt(null, BanReasons.RateLimit, NeverCalled));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ public class BlocklistConfigurationTests
|
|||
Assert.False(new BlocklistSettings().Enabled);
|
||||
}
|
||||
|
||||
// Null, not the old default array. FileAllowlist warns on a non-empty value, so a default would
|
||||
// Null, not the old default array. ManualAllowlist warns on a non-empty value, so a default would
|
||||
// fire that warning on every shard that never set it.
|
||||
[Fact]
|
||||
public void Deprecated_allowlist_files_defaults_to_null()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: FileAllowlistConfigurationTests.cs *
|
||||
* File: ManualAllowlistConfigurationTests.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
|
|
@ -19,16 +19,16 @@ using Server.Json;
|
|||
using Server.Network.Bans;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Network.Bans.Blocklist;
|
||||
namespace Server.Tests.Network.ManualAllowlists;
|
||||
|
||||
public class FileAllowlistConfigurationTests
|
||||
public class ManualAllowlistConfigurationTests
|
||||
{
|
||||
// Locks the JsonConfig casing contract: JsonConfig's options are case-SENSITIVE, so every settings
|
||||
// member must carry an explicit [JsonPropertyName("camelCase")] or it silently binds nothing.
|
||||
[Fact]
|
||||
public void FileAllowlistSettings_RoundTripsThroughJsonConfig()
|
||||
public void ManualAllowlistSettings_RoundTripsThroughJsonConfig()
|
||||
{
|
||||
var original = new FileAllowlistSettings
|
||||
var original = new ManualAllowlistSettings
|
||||
{
|
||||
Enabled = true,
|
||||
Files = ["D:/shared/ip-allowlist*.txt"],
|
||||
|
|
@ -41,7 +41,7 @@ public class FileAllowlistConfigurationTests
|
|||
Assert.Contains("\"files\"", json);
|
||||
Assert.Contains("\"reloadInterval\"", json);
|
||||
|
||||
var restored = JsonSerializer.Deserialize<FileAllowlistSettings>(json, JsonConfig.DefaultOptions);
|
||||
var restored = JsonSerializer.Deserialize<ManualAllowlistSettings>(json, JsonConfig.DefaultOptions);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(original.Enabled, restored.Enabled);
|
||||
|
|
@ -51,9 +51,9 @@ public class FileAllowlistConfigurationTests
|
|||
|
||||
// The point of the flag: a shard that never opts in must not start the reload poll.
|
||||
[Fact]
|
||||
public void File_allowlist_is_off_by_default()
|
||||
public void Manual_allowlist_is_off_by_default()
|
||||
{
|
||||
Assert.False(new FileAllowlistSettings().Enabled);
|
||||
Assert.False(new ManualAllowlistSettings().Enabled);
|
||||
}
|
||||
|
||||
// The generator creates ip-allowlist.txt beside the blocklist; the wildcard is what picks up a
|
||||
|
|
@ -61,6 +61,6 @@ public class FileAllowlistConfigurationTests
|
|||
[Fact]
|
||||
public void Default_pattern_matches_the_generator_output_path()
|
||||
{
|
||||
Assert.Equal(["Configuration/ip-allowlist*.txt"], new FileAllowlistSettings().Files);
|
||||
Assert.Equal(["Configuration/ip-allowlist*.txt"], new ManualAllowlistSettings().Files);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ using Server.Network.Bans;
|
|||
namespace Server.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Combines <see cref="FileAllowlist"/> and <see cref="LoginAllowlist"/> into the one answer
|
||||
/// Combines <see cref="ManualAllowlist"/> and <see cref="LoginAllowlist"/> into the one answer
|
||||
/// <see cref="BanChannel.IsExempt"/> asks for, so neither source has to know about the other.
|
||||
/// </summary>
|
||||
public static class BanExemptions
|
||||
|
|
@ -51,7 +51,7 @@ public static class BanExemptions
|
|||
}
|
||||
|
||||
// Deliberate and unconditional, so it wins and must not spend the earned list's strikes.
|
||||
if (FileAllowlist.Contains(address))
|
||||
if (ManualAllowlist.Contains(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ public record BlocklistSettings
|
|||
|
||||
/// <summary>
|
||||
/// Deprecated: moved to <c>files</c> in <c>ip-allowlist.json</c>, because the blocklist is only one of
|
||||
/// two consumers. Still bound so <see cref="FileAllowlist"/> can warn an operator who set it here
|
||||
/// two consumers. Still bound so <see cref="ManualAllowlist"/> can warn an operator who set it here
|
||||
/// instead of dropping the carve-out silently. Null when absent, which is the normal case.
|
||||
/// </summary>
|
||||
[JsonPropertyName("allowlistFiles")]
|
||||
|
|
|
|||
|
|
@ -102,10 +102,10 @@ public sealed class BlocklistFilter : IConnectionFilter
|
|||
|
||||
// The operator's override on this gate, opted into separately. Without it only the generator's
|
||||
// subtraction covers carve-outs, and that does not cover ban contributions.
|
||||
if (!FileAllowlist.Enabled)
|
||||
if (!ManualAllowlist.Enabled)
|
||||
{
|
||||
logger.Warning(
|
||||
"Blocklist is on but the file allowlist is not; set \"enabled\" in ip-allowlist.json so a " +
|
||||
"Blocklist is on but the manual allowlist is not; set \"enabled\" in ip-allowlist.json so a " +
|
||||
"carve-out also suppresses ban contributions"
|
||||
);
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ public sealed class BlocklistFilter : IConnectionFilter
|
|||
// Both are asked only once the list has matched, so they cost the common accept nothing. The file
|
||||
// list is usually redundant because the generator subtracts it — except right after an operator
|
||||
// adds an entry without regenerating, which is when someone is waiting to get back in.
|
||||
if (FileAllowlist.Contains(address))
|
||||
if (ManualAllowlist.Contains(address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ public sealed class BlocklistSnapshot
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plain set membership, for callers whose set is an ALLOWlist (see <see cref="FileAllowlist"/>) and for
|
||||
/// Plain set membership, for callers whose set is an ALLOWlist (see <see cref="ManualAllowlist"/>) and for
|
||||
/// whom <see cref="IsBanned"/> would read backwards. The interval machinery is direction-agnostic.
|
||||
/// </summary>
|
||||
public bool Contains(IPAddress ip) => IsBanned(ip);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ namespace Server.Network;
|
|||
/// <remarks>
|
||||
/// Consulted only after the blocklist has already matched, so a normal accept pays nothing for it. An entry
|
||||
/// is evidence rather than a licence: enough strikes inside the window revokes it. It cannot bootstrap, so
|
||||
/// it does not replace <see cref="FileAllowlist"/>. Both dictionaries are game-loop state; only the file
|
||||
/// it does not replace <see cref="ManualAllowlist"/>. Both dictionaries are game-loop state; only the file
|
||||
/// write runs off-loop, over a snapshot taken on the loop.
|
||||
/// See <c>dev-docs/ip-bans-and-allowlists.md</c>.
|
||||
/// </remarks>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: FileAllowlist.cs *
|
||||
* File: ManualAllowlist.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
|
|
@ -35,9 +35,9 @@ namespace Server.Network.Bans;
|
|||
/// next regeneration. Opt-in via <c>ip-allowlist.json</c>'s <c>enabled</c>, since the poll runs for the
|
||||
/// whole uptime; no shield against a manual ban either — see <see cref="BanExemptions"/>.
|
||||
/// </remarks>
|
||||
public static class FileAllowlist
|
||||
public static class ManualAllowlist
|
||||
{
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(FileAllowlist));
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ManualAllowlist));
|
||||
|
||||
// Written by the reload poll (off-loop), read by the accept path (game loop). One volatile reference
|
||||
// swap is the whole synchronization story: readers see the old or the new snapshot, whole.
|
||||
|
|
@ -58,8 +58,8 @@ public static class FileAllowlist
|
|||
|
||||
public static void Initialize()
|
||||
{
|
||||
FileAllowlistConfiguration.Load();
|
||||
var settings = FileAllowlistConfiguration.Settings;
|
||||
ManualAllowlistConfiguration.Load();
|
||||
var settings = ManualAllowlistConfiguration.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -81,14 +81,14 @@ public static class FileAllowlist
|
|||
if (present > 0)
|
||||
{
|
||||
logger.Warning(
|
||||
"File allowlist is off (\"enabled\" false in ip-allowlist.json) but {Count} allowlist file(s) " +
|
||||
"Manual allowlist is off (\"enabled\" false in ip-allowlist.json) but {Count} allowlist file(s) " +
|
||||
"are present; those carve-outs will not suppress ban contributions",
|
||||
present
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Information("File allowlist disabled (\"enabled\" false in ip-allowlist.json)");
|
||||
logger.Information("Manual allowlist disabled (\"enabled\" false in ip-allowlist.json)");
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -96,7 +96,7 @@ public static class FileAllowlist
|
|||
|
||||
if (_patterns.Length == 0)
|
||||
{
|
||||
logger.Information("File allowlist disabled (\"files\" empty in ip-allowlist.json)");
|
||||
logger.Information("Manual allowlist disabled (\"files\" empty in ip-allowlist.json)");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +242,7 @@ public static class FileAllowlist
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Warning(e, "File allowlist reload check failed; keeping last snapshot ({Count})", Count);
|
||||
logger.Warning(e, "Manual allowlist reload check failed; keeping last snapshot ({Count})", Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -291,7 +291,7 @@ public static class FileAllowlist
|
|||
_lastStamp = stamp;
|
||||
|
||||
logger.Information(
|
||||
"File allowlist loaded {Count} range(s) from {Files} file(s)",
|
||||
"Manual allowlist loaded {Count} range(s) from {Files} file(s)",
|
||||
next.Count,
|
||||
files
|
||||
);
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: FileAllowlistConfiguration.cs *
|
||||
* File: ManualAllowlistConfiguration.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
|
|
@ -21,14 +21,14 @@ using Server.Json;
|
|||
namespace Server.Network.Bans;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the <see cref="FileAllowlistSettings"/> from <c>Configuration/ip-allowlist.json</c>. Loaded once;
|
||||
/// Loads the <see cref="ManualAllowlistSettings"/> from <c>Configuration/ip-allowlist.json</c>. Loaded once;
|
||||
/// a missing file writes a template so operators have something to edit.
|
||||
/// </summary>
|
||||
public static class FileAllowlistConfiguration
|
||||
public static class ManualAllowlistConfiguration
|
||||
{
|
||||
private const string _path = "Configuration/ip-allowlist.json";
|
||||
|
||||
public static FileAllowlistSettings Settings { get; private set; }
|
||||
public static ManualAllowlistSettings Settings { get; private set; }
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
|
|
@ -36,11 +36,11 @@ public static class FileAllowlistConfiguration
|
|||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
Settings = JsonConfig.Deserialize<FileAllowlistSettings>(path);
|
||||
Settings = JsonConfig.Deserialize<ManualAllowlistSettings>(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings = new FileAllowlistSettings();
|
||||
Settings = new ManualAllowlistSettings();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
|
@ -52,11 +52,11 @@ public static class FileAllowlistConfiguration
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bound configuration for <see cref="FileAllowlist"/>. Its own file rather than a corner of
|
||||
/// Bound configuration for <see cref="ManualAllowlist"/>. Its own file rather than a corner of
|
||||
/// <c>blocklist.json</c>: the blocklist is only one of two consumers, and the other
|
||||
/// (<see cref="BanExemptions"/>) works on a shard that runs no blocklist at all.
|
||||
/// </summary>
|
||||
public record FileAllowlistSettings
|
||||
public record ManualAllowlistSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the shard reads <see cref="Files"/> at all. Off by default: reading them costs a poll for
|
||||
|
|
@ -38,7 +38,7 @@ Two, with different authority:
|
|||
|
||||
| List | Source | Revocable? | Covers |
|
||||
|---|---|---|---|
|
||||
| `FileAllowlist` | every `ip-allowlist*.txt` (opt-in) | No — an operator said so | Blocking **and** escalation |
|
||||
| `ManualAllowlist` | every `ip-allowlist*.txt` (opt-in) | No — an operator said so | Blocking **and** escalation |
|
||||
| `LoginAllowlist` | Earned by authenticating, 90-day TTL | Yes — 10 strikes/hour | Blocking **and** escalation |
|
||||
|
||||
Both are consulted **only after the blocklist has already matched**, so a normal accept — the one an
|
||||
|
|
@ -185,7 +185,7 @@ firewalled off. Shortening the 5s handshake window has been tried and broke real
|
|||
|
||||
- **An allowlist cannot bootstrap.** A `LoginAllowlist` entry is only earned by getting in, so it can never
|
||||
repair an existing false positive, and it is weakest on rotating CGNAT — a player whose lease moved is a
|
||||
stranger again. `FileAllowlist` is the fix for that, which is why it is manual — and opt-in, via
|
||||
stranger again. `ManualAllowlist` is the fix for that, which is why it is manual — and opt-in, via
|
||||
`ip-allowlist.json`.
|
||||
- **A never-logged-in player on a shared address can still be caught**, for up to `badConnectDuration`, if a
|
||||
co-tenant misbehaves. Accepted: it is 4h and self-healing. The cheapest lever is `badConnectDuration`.
|
||||
|
|
@ -218,7 +218,7 @@ A shard fronted by an upstream proxy can disable all of it and register nothing.
|
|||
| `Projects/Server/Network/Bans/BanReasons.cs` | Reason slugs + the behavioural opt-in set |
|
||||
| `Projects/UOContent/Network/BanExemptions.cs` | Combines both allowlists into one answer |
|
||||
| `Projects/UOContent/Network/Blocklist/BlocklistFilter.cs` | File-sourced blocklist filter |
|
||||
| `Projects/UOContent/Network/Blocklist/FileAllowlist.cs` | Operator carve-outs, read from the allowlist files |
|
||||
| `Projects/UOContent/Network/Blocklist/ManualAllowlist.cs` | Operator carve-outs, read from the allowlist files |
|
||||
| `Projects/UOContent/Network/LoginAllowlist/LoginAllowlist.cs` | Allowlist earned by authenticating |
|
||||
| `Projects/UOContent/Network/AutoDenylist/AutoDenylist.cs` | Short-lived local hold |
|
||||
| `Projects/UOContent/Network/CrowdSec/CrowdSecReporter.cs` | LAPI contribution sink |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue