fix(firewall): use Core.Now on the game thread; seed the test clock

Firewall.LoadFrom and ToSettings run on the game loop (Configure sweep, the
maintenance timer, Save on shutdown) but read DateTime.UtcNow. ToSettings was
the one that mattered: it derives each persisted expiry as
now + (expiresAtTick - nowTicks) while nowTicks came from Core.TickCount, so
pairing a fresh wall clock with the loop's tick baked the loop's lag into every
saved TTL. Core.Now and Core.TickCount are refreshed together at the top of each
iteration, so taking both keeps the operands on one instant.

Left DateTime.UtcNow in CrowdSecAlertClient and the reporter's flush/drain
paths, which run on the pool and have no loop clock to read.

Neither test fixture seeded Core._now, so Core.Now was DateTime.MinValue for the
whole test host -- MinValue.AddHours(-1) throws, and any code correctly reading
the game-thread clock computed nonsense. Seed it as Main.cs does.

Also fixes a dangling collection reference: the firewall tests moved into
UOContent.Tests still declared [Collection("Sequential Server Tests")], which is
only defined in Server.Tests. xUnit matched no fixture and silently skipped the
bootstrap for those tests.

Comment pass over the branch: drop development narration and tighten what stays.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-07-25 11:30:52 -07:00
parent 9df169946c
commit 99fc33ad47
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
12 changed files with 53 additions and 79 deletions

View file

@ -33,8 +33,7 @@ public static class BanConfiguration
public static void Configure()
{
// Reached by the Configure sweep, which is the only caller. Idempotent anyway, so a second call
// cannot re-deserialize or re-write the template over an operator's edits.
// Idempotent: a second call must not re-deserialize or overwrite an operator's edits.
if (Settings != null)
{
return;

View file

@ -75,8 +75,7 @@ public static class ConnectionFilters
var filters = _filters;
for (var i = 0; i < filters.Length; i++)
{
// Try/catch costs nothing when nothing throws, and this is the one path where a faulty
// third-party filter would otherwise take down the accept loop for every connection.
// A faulty filter must not take down the accept loop for every connection.
try
{
if (filters[i].ShouldDeny(address))
@ -96,10 +95,9 @@ public static class ConnectionFilters
}
/// <summary>
/// Drops a filter that threw on the accept path. A filter that faults once will fault for every
/// subsequent connection, so leaving it registered means an exception and a log line per accept —
/// exactly the amplification an attacker wants. Failing open here is deliberate: a broken filter
/// must not be able to deny every connection either.
/// Drops a filter that threw on the accept path: one that faults once faults for every subsequent
/// connection, costing an exception and a log line per accept. Failing open is deliberate — a broken
/// filter must not be able to deny every connection either.
/// </summary>
private static void Disable(IConnectionFilter filter, Exception e)
{