feat: make the blocklist and file allowlist opt-in, decouple their config
Both features ran on every shard out of the box, each polling on its own 60s timer for files most shards never generate. Neither was ever asked for. Blocklist: the only disable path was an empty "file", and the default is non-empty, so Start() always reached Task.Run(PollLoop) plus a recurring SweepGuard timer. Adds "enabled" (default false). A shard that has a list on disk but no "enabled" key logs a Warning rather than silently dropping a gate it was relying on. File allowlist: moves out of blocklist.json into its own ip-allowlist.json with "enabled" (default false), "files" and "reloadInterval". It was never a sub-feature of the blocklist -- its two consumers are BlocklistFilter, where the generator already subtracts these files anyway, and BanExemptions, which suppresses behavioural ban contributions and works on a shard running no blocklist at all. That second consumer is the only mechanism for what it does, so a shared flag could not express it. "allowlistFiles" stays bound on BlocklistSettings, defaulting to null and deliberately not honoured, purely so an operator who set it is told where it went instead of losing the carve-out silently. The two still work together: the blocklist warns at startup when it is on and the file allowlist is not, since only the generator's subtraction is covering carve-outs then, and that does not cover ban contributions. Also fixes the promote-guard sweep, which was recurring and tokenless, so Stop() cancelled the poll but left the sweep running and a later Start() added another. It is now held and stopped, and only started when hits are reported -- nothing can mark the guard otherwise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
240118340e
commit
19b8ce2ce4
7 changed files with 304 additions and 37 deletions
|
|
@ -30,6 +30,7 @@ public class BlocklistConfigurationTests
|
|||
{
|
||||
var original = new BlocklistSettings
|
||||
{
|
||||
Enabled = true,
|
||||
File = "D:/shared/ip-blocklist.txt",
|
||||
ReloadInterval = TimeSpan.FromMinutes(5),
|
||||
ReportHits = false,
|
||||
|
|
@ -39,6 +40,7 @@ public class BlocklistConfigurationTests
|
|||
|
||||
var json = JsonConfig.Serialize(original);
|
||||
|
||||
Assert.Contains("\"enabled\"", json);
|
||||
Assert.Contains("\"file\"", json);
|
||||
Assert.Contains("\"reloadInterval\"", json);
|
||||
Assert.Contains("\"reportHits\"", json);
|
||||
|
|
@ -48,6 +50,7 @@ public class BlocklistConfigurationTests
|
|||
var restored = JsonSerializer.Deserialize<BlocklistSettings>(json, JsonConfig.DefaultOptions);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(original.Enabled, restored.Enabled);
|
||||
Assert.Equal(original.File, restored.File);
|
||||
Assert.Equal(original.ReloadInterval, restored.ReloadInterval);
|
||||
Assert.Equal(original.ReportHits, restored.ReportHits);
|
||||
|
|
@ -55,6 +58,21 @@ public class BlocklistConfigurationTests
|
|||
Assert.Equal(original.PromoteSuppression, restored.PromoteSuppression);
|
||||
}
|
||||
|
||||
// The point of the flag: a shard that never opts in must not start the reload poll.
|
||||
[Fact]
|
||||
public void Blocklist_is_off_by_default()
|
||||
{
|
||||
Assert.False(new BlocklistSettings().Enabled);
|
||||
}
|
||||
|
||||
// Null, not the old default array. FileAllowlist 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()
|
||||
{
|
||||
Assert.Null(new BlocklistSettings().AllowlistFiles);
|
||||
}
|
||||
|
||||
// The generator (tools/Export-IpBlocklist.ps1) writes to this path by default; if one side moves
|
||||
// without the other, a shard silently enforces nothing.
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: FileAllowlistConfigurationTests.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 *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Server.Json;
|
||||
using Server.Network.Bans;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Network.Bans.Blocklist;
|
||||
|
||||
public class FileAllowlistConfigurationTests
|
||||
{
|
||||
// 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()
|
||||
{
|
||||
var original = new FileAllowlistSettings
|
||||
{
|
||||
Enabled = true,
|
||||
Files = ["D:/shared/ip-allowlist*.txt"],
|
||||
ReloadInterval = TimeSpan.FromMinutes(5)
|
||||
};
|
||||
|
||||
var json = JsonConfig.Serialize(original);
|
||||
|
||||
Assert.Contains("\"enabled\"", json);
|
||||
Assert.Contains("\"files\"", json);
|
||||
Assert.Contains("\"reloadInterval\"", json);
|
||||
|
||||
var restored = JsonSerializer.Deserialize<FileAllowlistSettings>(json, JsonConfig.DefaultOptions);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(original.Enabled, restored.Enabled);
|
||||
Assert.Equal(original.Files, restored.Files);
|
||||
Assert.Equal(original.ReloadInterval, restored.ReloadInterval);
|
||||
}
|
||||
|
||||
// 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()
|
||||
{
|
||||
Assert.False(new FileAllowlistSettings().Enabled);
|
||||
}
|
||||
|
||||
// The generator creates ip-allowlist.txt beside the blocklist; the wildcard is what picks up a
|
||||
// carve-out file (-RefreshCarveouts writes ip-allowlist-starlink.txt) with no config edit.
|
||||
[Fact]
|
||||
public void Default_pattern_matches_the_generator_output_path()
|
||||
{
|
||||
Assert.Equal(["Configuration/ip-allowlist*.txt"], new FileAllowlistSettings().Files);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,12 +53,19 @@ public static class BlocklistConfiguration
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bound configuration for <see cref="BlocklistFilter"/>. The filter is inert unless <see cref="File"/>
|
||||
/// points at a list that actually exists, so the shipped defaults are safe on a shard that never runs
|
||||
/// the generator.
|
||||
/// Bound configuration for <see cref="BlocklistFilter"/>. The filter is inert unless <see cref="Enabled"/>
|
||||
/// is set and <see cref="File"/> points at a list that actually exists, so the shipped defaults cost a
|
||||
/// shard that never runs the generator nothing at all.
|
||||
/// </summary>
|
||||
public record BlocklistSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the accept-path gate runs at all. Off by default: the reload poll runs for the whole
|
||||
/// uptime, which no shard should pay before an operator has chosen to run a blocklist.
|
||||
/// </summary>
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the blocklist. A relative path resolves against <see cref="Core.BaseDirectory"/>; an
|
||||
/// absolute path is used as-is (handy when several shards share one generated list). Set to
|
||||
|
|
@ -68,17 +75,12 @@ public record BlocklistSettings
|
|||
public string File { get; set; } = "Configuration/ip-blocklist.txt";
|
||||
|
||||
/// <summary>
|
||||
/// Addresses that must never be blocked and never escalated, in the blocklist's own format. The same
|
||||
/// files <c>tools/Export-IpBlocklist.ps1</c> subtracts at generation time; the shard reads them so an
|
||||
/// entry also suppresses ban contributions, which the generator alone cannot do. See
|
||||
/// <see cref="FileAllowlist"/>.
|
||||
/// 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
|
||||
/// instead of dropping the carve-out silently. Null when absent, which is the normal case.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The filename may contain wildcards, which is how the default picks up a carve-out an admin adds
|
||||
/// without anyone editing this file.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("allowlistFiles")]
|
||||
public string[] AllowlistFiles { get; set; } = ["Configuration/ip-allowlist*.txt"];
|
||||
public string[] AllowlistFiles { get; set; }
|
||||
|
||||
/// <summary>How often the file is checked for changes. Reloads only happen when it actually changed.</summary>
|
||||
[JsonPropertyName("reloadInterval")]
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ namespace Server.Network.Bans;
|
|||
/// <summary>
|
||||
/// Accept-path gate for a large, file-sourced IP blocklist, hydrated from the file a generator
|
||||
/// (<c>tools/Export-IpBlocklist.ps1</c>) writes on a schedule. Holds an immutable snapshot swapped
|
||||
/// atomically by an off-loop reload poll, so accept-path reads are lock-free. Inert when no file is
|
||||
/// configured or present.
|
||||
/// atomically by an off-loop reload poll, so accept-path reads are lock-free. Opt-in via
|
||||
/// <c>blocklist.json</c>'s <c>enabled</c>; inert when off, or when no file is configured or present.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the demand-paging half of the design: an OS firewall cannot hold millions of entries on
|
||||
|
|
@ -44,6 +44,7 @@ public sealed class BlocklistFilter : IConnectionFilter
|
|||
|
||||
private readonly PromotedGuard _guard = new();
|
||||
|
||||
private bool _enabled;
|
||||
private string _path;
|
||||
private TimeSpan _interval;
|
||||
private bool _reportHits;
|
||||
|
|
@ -52,6 +53,7 @@ public sealed class BlocklistFilter : IConnectionFilter
|
|||
private string _lastGenerated;
|
||||
private DateTime _lastWriteUtc;
|
||||
private CancellationTokenSource _cts;
|
||||
private Timer _sweepTimer;
|
||||
|
||||
public string Name => "blocklist";
|
||||
|
||||
|
|
@ -68,6 +70,7 @@ public sealed class BlocklistFilter : IConnectionFilter
|
|||
var s = BlocklistConfiguration.Settings;
|
||||
|
||||
_path = ResolvePath(s.File);
|
||||
_enabled = s.Enabled && _path != null;
|
||||
_interval = s.ReloadInterval <= TimeSpan.Zero ? TimeSpan.FromSeconds(60) : s.ReloadInterval;
|
||||
_reportHits = s.ReportHits;
|
||||
_banDuration = s.BanDuration;
|
||||
|
|
@ -91,16 +94,26 @@ public sealed class BlocklistFilter : IConnectionFilter
|
|||
|
||||
public void Start(CancellationToken token)
|
||||
{
|
||||
if (_path == null)
|
||||
if (!_enabled)
|
||||
{
|
||||
logger.Information("Blocklist disabled (\"file\" empty in blocklist.json)");
|
||||
LogWhyDisabled();
|
||||
return;
|
||||
}
|
||||
|
||||
// The allowlist is the operator's override on this gate, and it is opted into separately. Only the
|
||||
// generator's subtraction is covering them here, which does not cover ban contributions.
|
||||
if (!FileAllowlist.Enabled)
|
||||
{
|
||||
logger.Warning(
|
||||
"Blocklist is on but the file allowlist is not; set \"enabled\" in ip-allowlist.json so a " +
|
||||
"carve-out also suppresses ban contributions"
|
||||
);
|
||||
}
|
||||
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
|
||||
// A missing file is the shipped default, not an error: the gate stays inert until the poll picks
|
||||
// up whatever the generator first writes. No restart needed.
|
||||
// A missing file is not an error: the gate stays inert until the poll picks up whatever the
|
||||
// generator first writes. No restart needed.
|
||||
if (File.Exists(_path))
|
||||
{
|
||||
Reload(); // synchronous prime; empty on failure (fail-open)
|
||||
|
|
@ -110,17 +123,47 @@ public sealed class BlocklistFilter : IConnectionFilter
|
|||
logger.Information("Blocklist inert: no list at \"{Path}\"; polling every {Interval}", _path, _interval);
|
||||
}
|
||||
|
||||
// Sweep the promote-guard so a distinct-IP flood cannot grow it unbounded.
|
||||
Timer.DelayCall(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1), SweepGuard);
|
||||
// Sweep the promote-guard so a distinct-IP flood cannot grow it unbounded. Only marked when hits
|
||||
// are reported, so there is nothing to sweep otherwise.
|
||||
if (_reportHits)
|
||||
{
|
||||
_sweepTimer = Timer.DelayCall(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1), SweepGuard);
|
||||
}
|
||||
|
||||
_ = Task.Run(() => PollLoop(_cts.Token), _cts.Token);
|
||||
}
|
||||
|
||||
private void LogWhyDisabled()
|
||||
{
|
||||
if (_path == null)
|
||||
{
|
||||
logger.Information("Blocklist disabled (\"file\" empty in blocklist.json)");
|
||||
}
|
||||
else if (File.Exists(_path))
|
||||
{
|
||||
// A shard upgrading from before the flag has a list on disk but no "enabled" key, so say so
|
||||
// rather than silently dropping a gate it was relying on.
|
||||
logger.Warning(
|
||||
"Blocklist is off (\"enabled\" false in blocklist.json) but a list is present at \"{Path}\"; " +
|
||||
"no addresses will be denied",
|
||||
_path
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Information("Blocklist disabled (\"enabled\" false in blocklist.json)");
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
|
||||
// Recurring and tokenless before: a Stop/Start cycle left the old sweep running and added another.
|
||||
_sweepTimer?.Stop();
|
||||
_sweepTimer = null;
|
||||
}
|
||||
|
||||
public bool ShouldDeny(IPAddress address)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ namespace Server.Network.Bans;
|
|||
/// Behavioural detections never consult the blocklist, so without reading the files here a carve-out is
|
||||
/// quietly routed around: one scanner behind a shared CGNAT address is enough to get the whole address
|
||||
/// contributed and firewalled. Reading them also means an entry applies on the next reload rather than the
|
||||
/// next regeneration. Unconditional, unlike <see cref="LoginAllowlist"/>, but still no shield against a
|
||||
/// manual ban — see <see cref="BanExemptions"/>.
|
||||
/// next regeneration. Opt-in via <c>blocklist.json</c>'s <c>allowlistEnabled</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
|
||||
{
|
||||
|
|
@ -53,24 +53,54 @@ public static class FileAllowlist
|
|||
/// <summary>True when an operator listed this address. Safe before <see cref="Initialize"/>.</summary>
|
||||
public static bool Contains(IPAddress address) => address != null && _snapshot.Contains(address);
|
||||
|
||||
/// <summary>True when the shard is reading allowlist files. Safe before <see cref="Initialize"/>.</summary>
|
||||
public static bool Enabled { get; private set; }
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
// BlocklistFilter.Register ran during the Configure sweep, so the settings are populated.
|
||||
var settings = BlocklistConfiguration.Settings;
|
||||
FileAllowlistConfiguration.Load();
|
||||
var settings = FileAllowlistConfiguration.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_patterns = ResolvePaths(settings.AllowlistFiles);
|
||||
// Ran after the Configure sweep, so blocklist.json is loaded and the deprecated key is readable.
|
||||
WarnOnDeprecatedKey();
|
||||
|
||||
_patterns = ResolvePaths(settings.Files);
|
||||
_interval = settings.ReloadInterval <= TimeSpan.Zero ? TimeSpan.FromSeconds(60) : settings.ReloadInterval;
|
||||
|
||||
if (_patterns.Length == 0)
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
logger.Information("File allowlist disabled (\"allowlistFiles\" empty in blocklist.json)");
|
||||
// The generator still subtracts these files, so a carve-out an operator already wrote looks
|
||||
// like it works right up until a behavioural detection contributes the address anyway.
|
||||
var present = ExpandPaths().Length;
|
||||
_patterns = [];
|
||||
|
||||
if (present > 0)
|
||||
{
|
||||
logger.Warning(
|
||||
"File 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)");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_patterns.Length == 0)
|
||||
{
|
||||
logger.Information("File allowlist disabled (\"files\" empty in ip-allowlist.json)");
|
||||
return;
|
||||
}
|
||||
|
||||
Enabled = true;
|
||||
Reload();
|
||||
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(Core.ClosingTokenSource.Token);
|
||||
|
|
@ -84,6 +114,21 @@ public static class FileAllowlist
|
|||
_cts = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The setting moved out of <c>blocklist.json</c>. Deliberately not honoured from there — that would
|
||||
/// keep the coupling alive — but an operator who set it is told rather than losing it silently.
|
||||
/// </summary>
|
||||
private static void WarnOnDeprecatedKey()
|
||||
{
|
||||
if (BlocklistConfiguration.Settings?.AllowlistFiles is { Length: > 0 })
|
||||
{
|
||||
logger.Warning(
|
||||
"\"allowlistFiles\" in blocklist.json is ignored; it moved to \"files\" in ip-allowlist.json. " +
|
||||
"Copy it there and delete it from blocklist.json"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] ResolvePaths(string[] configured)
|
||||
{
|
||||
if (configured == null)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: FileAllowlistConfiguration.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 *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
using Server.Json;
|
||||
|
||||
namespace Server.Network.Bans;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the <see cref="FileAllowlistSettings"/> 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
|
||||
{
|
||||
private const string _path = "Configuration/ip-allowlist.json";
|
||||
|
||||
public static FileAllowlistSettings Settings { get; private set; }
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
var path = Path.Join(Core.BaseDirectory, _path);
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
Settings = JsonConfig.Deserialize<FileAllowlistSettings>(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings = new FileAllowlistSettings();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Save()
|
||||
{
|
||||
JsonConfig.Serialize(Path.Join(Core.BaseDirectory, _path), Settings);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bound configuration for <see cref="FileAllowlist"/>. 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the shard reads <see cref="Files"/> at all. Off by default: reading them costs a poll for
|
||||
/// the whole uptime, which no shard should pay before an operator has written a carve-out.
|
||||
/// </summary>
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Addresses that must never be blocked and never escalated, in the blocklist's own format. The same
|
||||
/// files <c>tools/Export-IpBlocklist.ps1</c> subtracts at generation time; the shard reads them so an
|
||||
/// entry also suppresses ban contributions, which the generator alone cannot do.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The filename may contain wildcards, which is how the default picks up a carve-out an admin adds
|
||||
/// without anyone editing this file.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("files")]
|
||||
public string[] Files { get; set; } = ["Configuration/ip-allowlist*.txt"];
|
||||
|
||||
/// <summary>How often the files are checked for changes. Reloads only happen when one actually changed.</summary>
|
||||
[JsonPropertyName("reloadInterval")]
|
||||
public TimeSpan ReloadInterval { get; set; } = TimeSpan.FromSeconds(60);
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ Filters are consulted in registration order, first denial wins:
|
|||
| Filter | Source | Scope |
|
||||
|---|---|---|
|
||||
| `firewall` | `Configuration/firewall.json`, mutable in-game | Admin-curated, permanent |
|
||||
| `blocklist` | `Configuration/ip-blocklist.txt` (millions of entries) | Reputation feeds |
|
||||
| `blocklist` | `Configuration/ip-blocklist.txt` (millions of entries, opt-in) | Reputation feeds |
|
||||
| `auto-denylist` | In-memory, 15 min | What this shard just caught misbehaving |
|
||||
|
||||
### Contributing a ban
|
||||
|
|
@ -38,7 +38,7 @@ Two, with different authority:
|
|||
|
||||
| List | Source | Revocable? | Covers |
|
||||
|---|---|---|---|
|
||||
| `FileAllowlist` | every `ip-allowlist*.txt` | No — an operator said so | Blocking **and** escalation |
|
||||
| `FileAllowlist` | 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
|
||||
|
|
@ -66,16 +66,22 @@ If none of those match, they may be inside a **CIDR** in the blocklist, or held
|
|||
|
||||
### 2. Add them to the allowlist
|
||||
|
||||
One entry per line in `Distribution/Configuration/ip-allowlist.txt` — a bare address or a CIDR. This file
|
||||
is yours; the generator creates it once and never rewrites it.
|
||||
Set `"enabled": true` in `Configuration/ip-allowlist.json` first — it is off by default, so a shard that
|
||||
has never written a carve-out does not poll for one. The shard logs a warning at startup if allowlist files
|
||||
are present while the flag is off.
|
||||
|
||||
Then one entry per line in `Distribution/Configuration/ip-allowlist.txt` — a bare address or a CIDR. This
|
||||
file is yours; the generator creates it once and never rewrites it.
|
||||
|
||||
```
|
||||
203.0.113.42 # shard owner, listed via a shared upstream address
|
||||
198.51.100.0/24 # a whole range if the ISP rotates within it
|
||||
```
|
||||
|
||||
The shard reloads within `reloadInterval` (60s default). **No restart, and no need to re-run the
|
||||
generator.** From that point the address is neither blocked nor contributed.
|
||||
With the flag on, the shard reloads within `reloadInterval` (60s default). **No restart, and no need to
|
||||
re-run the generator.** From that point the address is neither blocked nor contributed. With the flag off
|
||||
the generator still subtracts the file at generation time, so the address stops being *blocked* — but a
|
||||
behavioural detection can still contribute it, which is the case the flag exists to cover.
|
||||
|
||||
### 3. Clear any ban that already exists
|
||||
|
||||
|
|
@ -123,8 +129,10 @@ where your players actually are, and a carve-out names a real network, so you bu
|
|||
```
|
||||
|
||||
That writes `ip-allowlist-starlink.txt` beside the blocklist, and every `ip-allowlist*.txt` there is
|
||||
subtracted — both by the generator and by the shard, with no config edit. Starlink costs about 0.1% of the
|
||||
list. Blank a file (keep the file) to reputation-block that network again; delete it to drop the carve-out.
|
||||
subtracted by the generator with no config edit. For the shard to read them too — which is what also stops
|
||||
a carve-out address being *contributed* by a behavioural detection — set `enabled` in `ip-allowlist.json`;
|
||||
it is off by default so no shard polls for files it never wrote. Starlink costs about 0.1% of the list.
|
||||
Blank a file (keep the file) to reputation-block that network again; delete it to drop the carve-out.
|
||||
|
||||
Carve-out files carry an `asn=` marker in their header, which is how `-RefreshCarveouts` finds them. A
|
||||
hand-written allowlist has no marker and is never rewritten.
|
||||
|
|
@ -177,7 +185,8 @@ 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.
|
||||
stranger again. `FileAllowlist` 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`.
|
||||
- **`MaxConnections` (4096) is a hard ceiling.** The accept gate runs *after* the kernel completed the TCP
|
||||
|
|
@ -189,7 +198,8 @@ firewalled off. Shortening the 5s handshake window has been tried and broke real
|
|||
| File | Controls |
|
||||
|---|---|
|
||||
| `bans.json` | `reportRateLimitTrips`, `autoBanDuration`, `reportBadConnects`, `badConnectDuration` |
|
||||
| `blocklist.json` | `file`, `allowlistFiles` (wildcards allowed), `reloadInterval`, `reportHits`, `banDuration`, `promoteSuppression` |
|
||||
| `blocklist.json` | `enabled` (default `false`), `file`, `reloadInterval`, `reportHits`, `banDuration`, `promoteSuppression` |
|
||||
| `ip-allowlist.json` | `enabled` (default `false`), `files` (wildcards allowed), `reloadInterval` |
|
||||
| `login-allowlist.json` | `enabled`, `file`, `ttl`, `flushInterval`, `escalateAfterStrikes`, `strikeWindow` |
|
||||
| `auto-denylist.json` | `enabled`, `duration`, `maxEntries` |
|
||||
| `crowdsec.json` | `lapiUrl`, `machineId`, `password`, `origin`, `manualBanDuration`, `flushInterval`, `maxQueue` |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue