From bd187bd25288cc051867dc552b083fbb82ae4dcf Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:26:06 -0700 Subject: [PATCH] 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) --- .../Tests/Network/BanExemptionsTests.cs | 28 +++++++++---------- .../Blocklist/BlocklistConfigurationTests.cs | 2 +- .../ManualAllowlistConfigurationTests.cs} | 18 ++++++------ Projects/UOContent/Network/BanExemptions.cs | 4 +-- .../Blocklist/BlocklistConfiguration.cs | 2 +- .../Network/Blocklist/BlocklistFilter.cs | 6 ++-- .../Network/Blocklist/BlocklistSnapshot.cs | 2 +- .../Network/LoginAllowlist/LoginAllowlist.cs | 2 +- .../ManualAllowlist.cs} | 20 ++++++------- .../ManualAllowlistConfiguration.cs} | 16 +++++------ dev-docs/ip-bans-and-allowlists.md | 6 ++-- 11 files changed, 53 insertions(+), 53 deletions(-) rename Projects/UOContent.Tests/Tests/Network/{Bans/Blocklist/FileAllowlistConfigurationTests.cs => ManualAllowlist/ManualAllowlistConfigurationTests.cs} (78%) rename Projects/UOContent/Network/{Blocklist/FileAllowlist.cs => ManualAllowlist/ManualAllowlist.cs} (93%) rename Projects/UOContent/Network/{Blocklist/FileAllowlistConfiguration.cs => ManualAllowlist/ManualAllowlistConfiguration.cs} (83%) diff --git a/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs b/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs index 7e930fbdf..b38364373 100644 --- a/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs +++ b/Projects/UOContent.Tests/Tests/Network/BanExemptionsTests.cs @@ -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)); } diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistConfigurationTests.cs b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistConfigurationTests.cs index f1ec00686..0b89c2657 100644 --- a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistConfigurationTests.cs +++ b/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/BlocklistConfigurationTests.cs @@ -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() diff --git a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/FileAllowlistConfigurationTests.cs b/Projects/UOContent.Tests/Tests/Network/ManualAllowlist/ManualAllowlistConfigurationTests.cs similarity index 78% rename from Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/FileAllowlistConfigurationTests.cs rename to Projects/UOContent.Tests/Tests/Network/ManualAllowlist/ManualAllowlistConfigurationTests.cs index b99c0922d..1146414b5 100644 --- a/Projects/UOContent.Tests/Tests/Network/Bans/Blocklist/FileAllowlistConfigurationTests.cs +++ b/Projects/UOContent.Tests/Tests/Network/ManualAllowlist/ManualAllowlistConfigurationTests.cs @@ -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(json, JsonConfig.DefaultOptions); + var restored = JsonSerializer.Deserialize(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); } } diff --git a/Projects/UOContent/Network/BanExemptions.cs b/Projects/UOContent/Network/BanExemptions.cs index 5566bd5e7..c5f6b726a 100644 --- a/Projects/UOContent/Network/BanExemptions.cs +++ b/Projects/UOContent/Network/BanExemptions.cs @@ -20,7 +20,7 @@ using Server.Network.Bans; namespace Server.Network; /// -/// Combines and into the one answer +/// Combines and into the one answer /// asks for, so neither source has to know about the other. /// 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; } diff --git a/Projects/UOContent/Network/Blocklist/BlocklistConfiguration.cs b/Projects/UOContent/Network/Blocklist/BlocklistConfiguration.cs index 5461502d0..8033d1e67 100644 --- a/Projects/UOContent/Network/Blocklist/BlocklistConfiguration.cs +++ b/Projects/UOContent/Network/Blocklist/BlocklistConfiguration.cs @@ -75,7 +75,7 @@ public record BlocklistSettings /// /// Deprecated: moved to files in ip-allowlist.json, because the blocklist is only one of - /// two consumers. Still bound so can warn an operator who set it here + /// two consumers. Still bound so can warn an operator who set it here /// instead of dropping the carve-out silently. Null when absent, which is the normal case. /// [JsonPropertyName("allowlistFiles")] diff --git a/Projects/UOContent/Network/Blocklist/BlocklistFilter.cs b/Projects/UOContent/Network/Blocklist/BlocklistFilter.cs index 421809d01..030997803 100644 --- a/Projects/UOContent/Network/Blocklist/BlocklistFilter.cs +++ b/Projects/UOContent/Network/Blocklist/BlocklistFilter.cs @@ -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; } diff --git a/Projects/UOContent/Network/Blocklist/BlocklistSnapshot.cs b/Projects/UOContent/Network/Blocklist/BlocklistSnapshot.cs index 711b23ac1..d62df0752 100644 --- a/Projects/UOContent/Network/Blocklist/BlocklistSnapshot.cs +++ b/Projects/UOContent/Network/Blocklist/BlocklistSnapshot.cs @@ -182,7 +182,7 @@ public sealed class BlocklistSnapshot } /// - /// Plain set membership, for callers whose set is an ALLOWlist (see ) and for + /// Plain set membership, for callers whose set is an ALLOWlist (see ) and for /// whom would read backwards. The interval machinery is direction-agnostic. /// public bool Contains(IPAddress ip) => IsBanned(ip); diff --git a/Projects/UOContent/Network/LoginAllowlist/LoginAllowlist.cs b/Projects/UOContent/Network/LoginAllowlist/LoginAllowlist.cs index df28e7fcc..096a03a48 100644 --- a/Projects/UOContent/Network/LoginAllowlist/LoginAllowlist.cs +++ b/Projects/UOContent/Network/LoginAllowlist/LoginAllowlist.cs @@ -32,7 +32,7 @@ namespace Server.Network; /// /// 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 . Both dictionaries are game-loop state; only the file +/// it does not replace . Both dictionaries are game-loop state; only the file /// write runs off-loop, over a snapshot taken on the loop. /// See dev-docs/ip-bans-and-allowlists.md. /// diff --git a/Projects/UOContent/Network/Blocklist/FileAllowlist.cs b/Projects/UOContent/Network/ManualAllowlist/ManualAllowlist.cs similarity index 93% rename from Projects/UOContent/Network/Blocklist/FileAllowlist.cs rename to Projects/UOContent/Network/ManualAllowlist/ManualAllowlist.cs index f4ce4ea0d..d02d4ac45 100644 --- a/Projects/UOContent/Network/Blocklist/FileAllowlist.cs +++ b/Projects/UOContent/Network/ManualAllowlist/ManualAllowlist.cs @@ -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 ip-allowlist.json's enabled, since the poll runs for the /// whole uptime; no shield against a manual ban either — see . /// -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 ); diff --git a/Projects/UOContent/Network/Blocklist/FileAllowlistConfiguration.cs b/Projects/UOContent/Network/ManualAllowlist/ManualAllowlistConfiguration.cs similarity index 83% rename from Projects/UOContent/Network/Blocklist/FileAllowlistConfiguration.cs rename to Projects/UOContent/Network/ManualAllowlist/ManualAllowlistConfiguration.cs index f60d6627f..dd21fe97c 100644 --- a/Projects/UOContent/Network/Blocklist/FileAllowlistConfiguration.cs +++ b/Projects/UOContent/Network/ManualAllowlist/ManualAllowlistConfiguration.cs @@ -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; /// -/// Loads the from Configuration/ip-allowlist.json. Loaded once; +/// Loads the from Configuration/ip-allowlist.json. Loaded once; /// a missing file writes a template so operators have something to edit. /// -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(path); + Settings = JsonConfig.Deserialize(path); } else { - Settings = new FileAllowlistSettings(); + Settings = new ManualAllowlistSettings(); Save(); } } @@ -52,11 +52,11 @@ public static class FileAllowlistConfiguration } /// -/// Bound configuration for . Its own file rather than a corner of +/// Bound configuration for . Its own file rather than a corner of /// blocklist.json: the blocklist is only one of two consumers, and the other /// () works on a shard that runs no blocklist at all. /// -public record FileAllowlistSettings +public record ManualAllowlistSettings { /// /// Whether the shard reads at all. Off by default: reading them costs a poll for diff --git a/dev-docs/ip-bans-and-allowlists.md b/dev-docs/ip-bans-and-allowlists.md index fa6b58b0d..a57447cb0 100644 --- a/dev-docs/ip-bans-and-allowlists.md +++ b/dev-docs/ip-bans-and-allowlists.md @@ -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 |