Commit graph

6 commits

Author SHA1 Message Date
Kamron Batman
9df169946c
style: use indexed for over arrays/lists and [] initializers
Applies the house conventions consistently across this PR's own code rather
than leaving them half-applied: BanChannel had Register/Stop converted while
Start kept a foreach, and ConnectionFilters was foreach throughout.

Converted every foreach over an array or List, and while doing so hoisted the
_reporters/_filters static field reads into a local so the loops match the
snapshot pattern Report/Retract/ShouldDeny already use.

Left as foreach where there is no indexer: the Dictionary walks in
Firewall.ExpireEntries, PromotedGuard.Sweep and BuildAlerts, and BuildAlerts'
IEnumerable parameter.

Dictionary field initializers become [] (it compiles, same lowering). The three
remaining new List<T>(capacity) calls keep their form -- a collection
expression cannot carry the capacity hint, and all three size the list exactly.

Scoped to files this PR authored or moved; pre-existing loops in AdminGump,
Main and Utility are left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 11:17:33 -07:00
Kamron Batman
2a1d345223
Cleanup 2026-07-25 10:57:07 -07:00
Kamron Batman
4d64a854c2
Simplifies Configure() 2026-07-25 10:52:06 -07:00
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
Kamron Batman
710973a28b
refactor(network): pluggable accept-path filters; move the blocklist to UOContent
Introduces IConnectionFilter and the ConnectionFilters registry: one seam the
accept path consults per inbound socket, so core no longer has to know where a
gate's data comes from. The registry is a plain array walked by an indexed loop,
so the hot path has no enumerator, no closure and no allocation; an interface
dispatch is noise next to the accept syscall. Filters register during the
Configure sweep, cheapest first, and the first denial short-circuits.

A filter that throws on the accept path is unregistered and the connection
fails open. A filter that faults once faults for every subsequent connection,
so leaving it registered would mean an exception and a log line per accept --
exactly the amplification an attacker wants -- and a broken filter must not be
able to deny every connection either.

With that seam in place the whole file blocklist moves to UOContent: it is
policy (which feeds, when to promote, what to report) built on an external file
format with an external producer, and core does not need any of it. Firewall
stays in core -- it is long-standing public API, it is what an admin reaches for
manually, and a shard running without UOContent still has to be able to block an
address -- but it now reaches the accept path through the same registry via a
small adapter, so the two remain separate implementations rather than one
conflated store.

Blocklist policy moves out of bans.json into a UOContent Configuration/
blocklist.json, next to crowdsec.json. bans.json keeps only what core decides:
reportRateLimitTrips and autoBanDuration.

Cleanups found while moving the code:

- BanChannel.Stop() persisted the Firewall. A contribution coordinator has no
  business saving an enforcement store; that is now the firewall filter's Stop.
- BlocklistGate.Evaluate took a `whitelisted` flag that was hardcoded false at
  its only call site, and no whitelist concept exists anywhere in core. Dropped.
- FileBlocklist was a static holding a snapshot and a promote-guard, which
  forced its tests onto the sequential collection and a LoadForTesting reset
  hook. It is now an instance, so each test owns its own state and they run in
  parallel. BlocklistGate folds into it: the pure decision survives as an
  internal Evaluate, which is all the separate type ever provided.
- The promote-guard sweep timer was registered from NetState.Configure, two
  files from the guard it swept, and ran even with the blocklist disabled. It
  now starts with the filter that owns it.

Core sheds ~570 source and ~340 test lines for ~90 of seam. 1344 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 00:55:42 -07:00
Kamron Batman
6249a20f15
feat(bans): contribute-first ban channel with CrowdSec reporter
Add a pluggable ban seam: BanChannel fans locally-decided bans out to
IBanReporter sinks (Report/Retract) and the accept path enforces locally.
CrowdSec is a write-only reporter living in UOContent (registered into the
Core seam via BanChannel.Register) — it batches decisions onto a bounded,
coalescing, drop-on-overflow queue and POSTs /v1/alerts with watcher creds,
retry/backoff, and a bounded flush-on-stop. CrowdSec never enforces in-app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:37:42 -07:00