/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: FileBlocklist.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 . * *************************************************************************/ using System; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using Server.Logging; namespace Server.Network.Bans.Blocklist; /// /// In-app gate for a large, file-sourced IP blocklist. Holds an immutable snapshot swapped atomically /// by an off-loop reload poll; accept-path reads are lock-free. Inert when no file is configured. /// public static class FileBlocklist { private static readonly ILogger logger = LogFactory.GetLogger(typeof(FileBlocklist)); private static volatile BlocklistSnapshot _snapshot = BlocklistSnapshot.Empty; private static string _path; private static TimeSpan _interval; private static string _lastGenerated; private static DateTime _lastWriteUtc; private static CancellationTokenSource _cts; public static int Count => _snapshot.Count; public static void Configure() { BanConfiguration.Configure(); var s = BanConfiguration.Settings; _path = s.BlocklistFile; _interval = s.BlocklistReloadInterval <= TimeSpan.Zero ? TimeSpan.FromSeconds(60) : s.BlocklistReloadInterval; } public static void Start(CancellationToken token) { if (string.IsNullOrWhiteSpace(_path)) { logger.Information("FileBlocklist disabled (blocklistFile empty in bans.json)"); return; } _cts = CancellationTokenSource.CreateLinkedTokenSource(token); Reload(); // synchronous prime; empty on failure (fail-open) _ = Task.Run(() => PollLoop(_cts.Token), _cts.Token); } public static void Stop() { _cts?.Cancel(); _cts?.Dispose(); _cts = null; } public static bool IsBanned(IPAddress ip) => _snapshot.IsBanned(ip); // Test hook: inject a snapshot without file I/O. public static void LoadForTesting(BlocklistSnapshot snapshot) => _snapshot = snapshot; private static async ValueTask PollLoop(CancellationToken token) { while (!token.IsCancellationRequested) { try { await Task.Delay(_interval, token); } catch (OperationCanceledException) { return; } try { if (ChangedSinceLastLoad()) { while (World.Saving || World.WorldState == WorldState.PendingSave) { await Task.Delay(TimeSpan.FromSeconds(1), token); } Reload(); } } catch (OperationCanceledException) { return; } catch (Exception e) { logger.Warning(e, "FileBlocklist reload check failed; keeping last snapshot ({Count})", Count); } } } private static bool ChangedSinceLastLoad() { try { var info = new FileInfo(_path); if (!info.Exists) { return false; } if (info.LastWriteTimeUtc == _lastWriteUtc) { return false; // cheapest guard } } catch { return false; } return !BlocklistFile.TryReadHeader(_path, out var h) || h.Generated != _lastGenerated; } private static void Reload() { // Capture the mtime/header BEFORE Load() so the markers describe the version we're about to // parse, not whatever the producer may have atomically swapped in mid-parse. If a swap happens // mid-parse, the markers describe the old-or-equal version, so the next poll detects the change // and reloads again -- this errs toward reloading and never skips a version. var writeUtc = default(DateTime); try { writeUtc = new FileInfo(_path).LastWriteTimeUtc; } catch { /* keep default */ } BlocklistFile.TryReadHeader(_path, out var h); var next = BlocklistFile.Load(_path, out var parsed, out var skipped); _snapshot = next; // single volatile swap; readers see old or new whole _lastGenerated = h.Generated; _lastWriteUtc = writeUtc; logger.Information("FileBlocklist loaded {Parsed} entr(ies) ({Count} ranges, {Skipped} skipped) gen={Gen}", parsed, next.Count, skipped, h.Generated); } }