ModernUO/Projects/Server/Network/Bans/BanChannel.cs
Kamron Batman f3b7c7451c
refactor(bans): drop BanChannel.Configure, which only re-ran a swept Configure
BanConfiguration.Configure() is a static Configure() on a static class, so the
AssemblyHandler sweep already reaches it. BanChannel.Configure() did nothing but
call it -- and was itself swept AND called explicitly from NetState.Configure(),
so the same load was reached three ways.

Nothing reads BanConfiguration.Settings during the Configure phase (only the
accept path does, long after the sweep), so there was no ordering guarantee to
preserve here. Removed the method and the explicit call.

The equivalent-looking calls in BlocklistFilter.Configure() and
CrowdSecReporter.Configure() are NOT the same thing and stay: those are instance
methods the sweep never sees, invoked from Register(), and each reads its
settings immediately -- so calling their configuration loader first is a real
ordering guarantee, made free by the same idempotency guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 02:04:56 -07:00

138 lines
4.7 KiB
C#

/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: BanChannel.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.Collections.Generic;
using System.Net;
using System.Threading;
using Server.Logging;
namespace Server.Network.Bans;
/// <summary>
/// Coordinates the configured <see cref="IBanReporter"/> contribution sinks. Enforcement is NOT here —
/// the accept path asks <see cref="ConnectionFilters"/>. This channel only fans locally-decided bans out
/// to external systems (CrowdSec), which distribute them to OS-level bouncers.
/// </summary>
public static class BanChannel
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BanChannel));
private static IBanReporter[] _reporters = [];
public static IReadOnlyList<IBanReporter> Reporters => _reporters;
/// <summary>
/// Registers a contribution sink from content (inversion of control). Idempotent by
/// <see cref="IBanReporter.Name"/>: a second registration of the same name is ignored. Configures the
/// reporter immediately so it is ready before <see cref="Start"/>.
/// </summary>
public static void Register(IBanReporter reporter)
{
if (reporter == null)
{
return;
}
foreach (var existing in _reporters)
{
if (existing.Name == reporter.Name)
{
return;
}
}
reporter.Configure();
var updated = new IBanReporter[_reporters.Length + 1];
Array.Copy(_reporters, updated, _reporters.Length);
updated[^1] = reporter;
_reporters = updated;
logger.Information("Ban channel registered reporter '{Name}'", reporter.Name);
}
internal static void ConfigureForTesting(IBanReporter[] reporters) => _reporters = reporters ?? [];
public static void Start(CancellationToken token)
{
foreach (var reporter in _reporters)
{
try
{
reporter.Start(token);
}
catch (Exception e)
{
// A broken contribution path must not crash boot — enforcement is local and unaffected.
logger.Error(e, "Ban reporter '{Name}' failed to start; continuing without it", reporter.Name);
}
}
}
public static void Stop()
{
foreach (var reporter in _reporters)
{
try
{
reporter.Stop();
}
catch (Exception e)
{
logger.Warning(e, "Ban reporter '{Name}' threw while stopping", reporter.Name);
}
}
}
/// <summary>Fans a locally-decided ban out to every reporter. Non-blocking; never throws.</summary>
public static void Report(IPAddress ip, TimeSpan ttl, string reason)
{
var reporters = _reporters;
for (var i = 0; i < reporters.Length; i++)
{
try
{
reporters[i].Report(ip, ttl, reason);
}
catch (Exception e)
{
logger.Warning(e, "Ban reporter '{Name}' threw during Report", reporters[i].Name);
}
}
}
/// <summary>Fans a retraction (manual unban) out to every retract-capable reporter.</summary>
public static void Retract(IPAddress ip)
{
var reporters = _reporters;
for (var i = 0; i < reporters.Length; i++)
{
if (!reporters[i].CanRetract)
{
continue;
}
try
{
reporters[i].Retract(ip);
}
catch (Exception e)
{
logger.Warning(e, "Ban reporter '{Name}' threw during Retract", reporters[i].Name);
}
}
}
}