ModernUO/Projects/UOContent/Network/Blocklist/BlocklistConfiguration.cs
Kamron Batman 19b8ce2ce4
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>
2026-08-13 21:07:55 -07:00

105 lines
4.6 KiB
C#

/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: BlocklistConfiguration.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="BlocklistSettings"/> from <c>Configuration/blocklist.json</c> (matching the
/// per-feature JSON config pattern used by <c>AssistantConfiguration</c>). Loaded once; a missing file
/// writes a template so operators have something to edit.
/// </summary>
public static class BlocklistConfiguration
{
private const string _path = "Configuration/blocklist.json";
public static BlocklistSettings Settings { get; private set; }
public static void Load()
{
var path = Path.Join(Core.BaseDirectory, _path);
if (File.Exists(path))
{
Settings = JsonConfig.Deserialize<BlocklistSettings>(path);
}
else
{
Settings = new BlocklistSettings();
Save();
}
}
private static void Save()
{
JsonConfig.Serialize(Path.Join(Core.BaseDirectory, _path), Settings);
}
}
/// <summary>
/// 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
/// <c>""</c> to disable the gate entirely. Produce the file with <c>tools/Export-IpBlocklist.ps1</c>.
/// </summary>
[JsonPropertyName("file")]
public string File { get; set; } = "Configuration/ip-blocklist.txt";
/// <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
/// instead of dropping the carve-out silently. Null when absent, which is the normal case.
/// </summary>
[JsonPropertyName("allowlistFiles")]
public string[] AllowlistFiles { get; set; }
/// <summary>How often the file is checked for changes. Reloads only happen when it actually changed.</summary>
[JsonPropertyName("reloadInterval")]
public TimeSpan ReloadInterval { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>Whether blocklist hits are contributed to the ban channel (the demand-paging promotion).</summary>
[JsonPropertyName("reportHits")]
public bool ReportHits { get; set; } = true;
/// <summary>Duration reported for a blocklist-matched ban.</summary>
[JsonPropertyName("banDuration")]
public TimeSpan BanDuration { get; set; } = TimeSpan.FromHours(6);
/// <summary>
/// How long the accept-path guard suppresses re-reporting a promoted address. This only needs to
/// bridge the gap until the OS bouncer picks up the promotion (seconds); after that the kernel drops
/// repeat traffic. Decoupled from <see cref="BanDuration"/> so the guard doesn't have to remember
/// hours' worth of distinct addresses.
/// </summary>
[JsonPropertyName("promoteSuppression")]
public TimeSpan PromoteSuppression { get; set; } = TimeSpan.FromSeconds(60);
}