/************************************************************************* * ModernUO * * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: IPRateLimiter.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.Collections.Concurrent; using System.Net; using System.Threading; using System.Threading.Tasks; namespace Server.Network; public class IPRateLimiter { private static readonly ConcurrentQueue _statsPool = []; private const int MaxPoolSize = 32_768; private readonly SemaphoreSlim _cleanupSignal = new(0, 1); private readonly ConcurrentDictionary _ipAttempts; private readonly ConcurrentQueue _cleanupQueue; private readonly CancellationTokenSource _cts; private readonly int _maxAttempts; private readonly long _timeWindow; // milliseconds private readonly long _initialBackoff; // milliseconds private readonly double _backoffMultiplier; private readonly long _maxBackoff; // milliseconds public IPRateLimiter( int maxAttempts, long timeWindow, long initialBackoff, double backoffMultiplier, long maxBackoff, CancellationToken token ) { _ipAttempts = []; _cleanupQueue = []; _maxAttempts = maxAttempts; _timeWindow = timeWindow; _initialBackoff = initialBackoff; _backoffMultiplier = backoffMultiplier; _maxBackoff = maxBackoff; _cts = CancellationTokenSource.CreateLinkedTokenSource(token); Task.Run(CleanupLoop, Core.ClosingTokenSource.Token); } public bool Verify(IPAddress ip, out int totalAttempts) { var nowTicks = Core.TickCount; var ipStats = _ipAttempts.GetOrAdd(ip, _ => GetOrCreateIPStats()); var added = ipStats.AttemptCount == 0; lock (ipStats) { if (nowTicks - ipStats.LastAttemptTicks > _timeWindow) { ipStats.AttemptCount = 1; // Reset } else { ipStats.AttemptCount++; } totalAttempts = ipStats.AttemptCount; ipStats.LastAttemptTicks = nowTicks; if (ipStats.BlockUntilTicks - nowTicks > 0) { return false; } if (ipStats.AttemptCount > _maxAttempts) { var backoffTime = Math.Min( (long)(_initialBackoff * Math.Pow(_backoffMultiplier, ipStats.AttemptCount - _maxAttempts)), _maxBackoff ); ipStats.BlockUntilTicks = nowTicks + backoffTime; return false; } if (added) { _cleanupQueue.Enqueue(ip); RunCleanup(); } } return true; } private static IPStats GetOrCreateIPStats() => _statsPool.TryDequeue(out var stats) ? stats : new IPStats(); private static void ReturnToPool(IPStats stats) { stats.Reset(); if (_statsPool.Count < MaxPoolSize) { _statsPool.Enqueue(stats); } } private void RunCleanup() { if (_cleanupSignal.CurrentCount > 0) { return; } try { _cleanupSignal.Release(); Task.Run(CleanupLoop, _cts.Token); } catch { // Do nothing } } private async ValueTask CleanupLoop() { while (!_cts.IsCancellationRequested) { await _cleanupSignal.WaitAsync(_cts.Token); var maxToProcess = Math.Min(_cleanupQueue.Count, 500); var nowTicks = Core.TickCount; for (var i = 0; i < maxToProcess; i++) { if (!_cts.IsCancellationRequested) { break; } if (!_cleanupQueue.TryDequeue(out var ip) || !_ipAttempts.TryGetValue(ip, out var ipStats)) { continue; } lock (ipStats) { if (nowTicks - ipStats.LastAttemptTicks < _timeWindow) { _cleanupQueue.Enqueue(ip); continue; } if (_ipAttempts.TryRemove(ip, out _)) { ReturnToPool(ipStats); } } } if (!_cleanupQueue.IsEmpty) { await Task.Delay(TimeSpan.FromMinutes(1), _cts.Token); try { _cleanupSignal.Release(); } catch { // Do nothing } } } } private class IPStats { public int AttemptCount; public long LastAttemptTicks; public long BlockUntilTicks; public void Reset() { AttemptCount = 0; LastAttemptTicks = 0; BlockUntilTicks = 0; } } }