From 2a8c62e8be1b1757765953c3889930f239032d1d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:19:38 -0800 Subject: [PATCH] fix: Fixes TCPServer accept async, makes Firewall/IP Limiter multithreaded (#2134) --- .../Tests/Network/Firewall/FirewallTests.cs | 137 ++++++++++++ Projects/Server/Client/ClientVersion.cs | 1 + Projects/Server/Client/UOClient.cs | 2 + Projects/Server/EventLoopTasks.cs | 35 +++- Projects/Server/Network/Firewall/Firewall.cs | 117 ++++++++--- .../Server/Network/Firewall/IFirewallEntry.cs | 4 +- Projects/Server/Network/IPLimiter.cs | 108 ---------- Projects/Server/Network/IPRateLimiter.cs | 197 ++++++++++++++++++ Projects/Server/Network/TcpServer.cs | 112 ++++++---- Projects/Server/Server.csproj | 3 + .../Gumps/TestGumps/StaticLayoutTestGump.cs | 2 - Projects/UOContent/Gumps/AdminGump.cs | 32 ++- Projects/UOContent/Misc/AdminFirewall.cs | 11 +- Projects/UOContent/Misc/ClientVerification.cs | 30 ++- .../Network/Packets/IncomingAccountPackets.cs | 2 +- 15 files changed, 578 insertions(+), 215 deletions(-) create mode 100644 Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs delete mode 100644 Projects/Server/Network/IPLimiter.cs create mode 100644 Projects/Server/Network/IPRateLimiter.cs diff --git a/Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs b/Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs new file mode 100644 index 000000000..af0f877e2 --- /dev/null +++ b/Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs @@ -0,0 +1,137 @@ + +using System.Net; +using System.Threading.Tasks; +using Server.Network; +using Xunit; + +namespace Server.Tests; + +public class FirewallTests +{ + [Fact] + public void Firewall_BlocksIPAddress_WhenAdded() + { + var ip = IPAddress.Parse("192.168.1.1"); + var entry = new SingleIpFirewallEntry("192.168.1.1"); + + Assert.False(Firewall.IsBlocked(ip)); + + Firewall.Add(entry); + + Assert.True(Firewall.IsBlocked(ip)); + } + + [Fact] + public void Firewall_DoesNotBlockIPAddress_WhenNotAdded() + { + var ip = IPAddress.Parse("192.168.1.2"); + Assert.False(Firewall.IsBlocked(ip)); + } + + [Fact] + public void Firewall_StopsBlockingIPAddress_WhenRemoved() + { + var ip = IPAddress.Parse("192.168.1.3"); + var entry = new SingleIpFirewallEntry("192.168.1.3"); + + Firewall.Add(entry); + Assert.True(Firewall.IsBlocked(ip)); + + Firewall.Remove(entry); + Assert.False(Firewall.IsBlocked(ip)); + } + + [Fact] + public void Firewall_BlocksIPRange() + { + var entry = new CidrFirewallEntry(IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.5")); + + Firewall.Add(entry); + + Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.1"))); + Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.3"))); + Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.5"))); + + Assert.False(Firewall.IsBlocked(IPAddress.Parse("10.0.0.6"))); + } + + [Fact] + public void Firewall_CacheInvalidation_WorksOnUpdate() + { + var ip = IPAddress.Parse("192.168.1.10"); + var entry = new SingleIpFirewallEntry("192.168.1.10"); + + Firewall.Add(entry); + Assert.True(Firewall.IsBlocked(ip)); + + Firewall.Remove(entry); + Assert.False(Firewall.IsBlocked(ip)); + } + + [Fact] + public void Firewall_ReadsFirewallSetCorrectly() + { + var entry = new SingleIpFirewallEntry("172.16.0.1"); + Firewall.Add(entry); + + bool found = false; + Firewall.ReadFirewallSet(set => + { + found = set.Contains(entry); + }); + + Assert.True(found); + } + + [Fact] + public void Firewall_IsThreadSafe() + { + IPAddress[] testIps = new IPAddress[256]; + for (int i = 0; i <= 255; i++) + { + testIps[i] = IPAddress.Parse($"192.168.0.{i}"); + } + + var entry = new CidrFirewallEntry(IPAddress.Parse("192.168.0.1"), IPAddress.Parse("192.168.0.255")); + Firewall.Add(entry); + + Parallel.ForEach(testIps, ip => + { + bool shouldBlock = int.Parse(ip.ToString().Split('.')[3]) is > 0; + Assert.Equal(shouldBlock, Firewall.IsBlocked(ip)); + }); + + Firewall.Remove(entry); + + Parallel.ForEach(testIps, ip => + { + Assert.False(Firewall.IsBlocked(ip)); + }); + } + + [Fact] + public void Firewall_DoesNotThrowWhenRemovingNonExistentEntry() + { + var entry = new SingleIpFirewallEntry("203.0.113.5"); + Assert.False(Firewall.Remove(entry)); + } + + [Fact] + public void Firewall_CacheHandlesMultipleUpdates() + { + var ip = IPAddress.Parse("192.168.1.20"); + var entry = new SingleIpFirewallEntry("192.168.1.20"); + + Firewall.Add(entry); + Assert.True(Firewall.IsBlocked(ip)); + + Firewall.Remove(entry); + Assert.False(Firewall.IsBlocked(ip)); + + Firewall.Add(entry); + Assert.True(Firewall.IsBlocked(ip)); + + Firewall.Remove(entry); + Assert.False(Firewall.IsBlocked(ip)); + } +} diff --git a/Projects/Server/Client/ClientVersion.cs b/Projects/Server/Client/ClientVersion.cs index 008650aae..37cad2f5c 100644 --- a/Projects/Server/Client/ClientVersion.cs +++ b/Projects/Server/Client/ClientVersion.cs @@ -30,6 +30,7 @@ public class ClientVersion : IComparable, IComparer _queue; - private readonly Thread _mainThread; - - public EventLoopContext() + public enum Priority { - _queue = new ConcurrentQueue(); + Normal, + High + } + + private readonly ConcurrentQueue _queue; + private readonly ConcurrentQueue _priorityQueue; + private readonly Thread _mainThread; + private readonly int _maxPerFrame; + + public EventLoopContext(int maxPerFrame = 128) + { + _maxPerFrame = maxPerFrame; + _queue = []; + _priorityQueue = []; _mainThread = Thread.CurrentThread; } public override SynchronizationContext CreateCopy() => new EventLoopContext(); - public void Post(Action d) => _queue.Enqueue(d); + public void Post(Action d, Priority priority = Priority.Normal) => + (priority == Priority.High ? _priorityQueue : _queue).Enqueue(d); public override void Post(SendOrPostCallback d, object state) => _queue.Enqueue(() => d(state)); @@ -62,7 +73,17 @@ public sealed class EventLoopContext : SynchronizationContext throw new Exception("Called EventLoop.ExecuteTasks on incorrect thread!"); } - var count = _queue.Count; + var count = _priorityQueue.Count; + + for (int i = 0; i < count; i++) + { + if (_priorityQueue.TryDequeue(out var a)) + { + a(); + } + } + + count = Math.Min(_queue.Count, _maxPerFrame); for (int i = 0; i < count; i++) { diff --git a/Projects/Server/Network/Firewall/Firewall.cs b/Projects/Server/Network/Firewall/Firewall.cs index e80050416..3321b5f46 100644 --- a/Projects/Server/Network/Firewall/Firewall.cs +++ b/Projects/Server/Network/Firewall/Firewall.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2024 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Firewall.cs * * * @@ -14,28 +14,44 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Threading; namespace Server.Network; public static class Firewall { + [ThreadStatic] private static InternalValidationEntry _validationEntry; - private static readonly Dictionary _isBlockedCache = new(); + private static readonly ConcurrentDictionary _isBlockedCache = []; + private static readonly ReaderWriterLockSlim _firewallLock = new(LockRecursionPolicy.NoRecursion); - private static readonly SortedSet _firewallSet = new(); + private static int _firewallVersion; + private static readonly SortedSet _firewallSet = []; - public static SortedSet FirewallSet => _firewallSet; + public static int FirewallSetCount => _firewallSet.Count; + + public static void ReadFirewallSet(Action> callback) + { + _firewallLock.EnterReadLock(); + try + { + callback(_firewallSet); + } + finally + { + _firewallLock.ExitReadLock(); + } + } internal static bool IsBlocked(IPAddress address) { - ref var isBlocked = ref CollectionsMarshal.GetValueRefOrAddDefault(_isBlockedCache, address, out var exists); - if (exists) + if (_isBlockedCache.TryGetValue(address, out var blockVersion) && blockVersion == _firewallVersion) { - return isBlocked; + return true; } if (_validationEntry == null) @@ -47,34 +63,68 @@ public static class Firewall _validationEntry.Address = address; } - // Get all entries that are lower than our validation entry - var view = _firewallSet.GetViewBetween(_firewallSet.Min, _validationEntry); - - // Loop backward since there shouldn't be any entries where the Min address is higher than ours - foreach (var firewallEntry in view.Reverse()) + if (CheckBlocked(_validationEntry)) { - if (firewallEntry.IsBlocked(_validationEntry.MinIpAddress)) - { - isBlocked = true; - return true; - } + _isBlockedCache[address] = _firewallVersion; + return true; } - isBlocked = view.Max?.IsBlocked(_validationEntry.MinIpAddress) == true; + return false; + } - return isBlocked; + private static bool CheckBlocked(IFirewallEntry validationEntry) + { + if (_firewallSet.Count == 0) + { + return false; + } + + _firewallLock.EnterReadLock(); + try + { + var min = _firewallSet.Min; + if (validationEntry.CompareTo(min) < 0) + { + return false; + } + + // Get all entries that are lower than our validation entry + var view = _firewallSet.GetViewBetween(min, validationEntry); + + // Loop backward since there shouldn't be any entries where the Min address is higher than ours + foreach (var firewallEntry in view.Reverse()) + { + if (firewallEntry.IsBlocked(validationEntry.MinIpAddress)) + { + return true; + } + } + + return view.Max?.IsBlocked(validationEntry.MinIpAddress) == true; + } + finally + { + _firewallLock.ExitReadLock(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Add(IFirewallEntry firewallEntry) { - if (_firewallSet.Add(firewallEntry)) + _firewallLock.EnterWriteLock(); + try { - _isBlockedCache.Clear(); - return true; + if (_firewallSet.Add(firewallEntry)) + { + Interlocked.Increment(ref _firewallVersion); // Update version + return true; + } + return false; + } + finally + { + _firewallLock.ExitWriteLock(); } - - return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -85,13 +135,20 @@ public static class Firewall return false; } - if (_firewallSet.Remove(entry)) + _firewallLock.EnterWriteLock(); + try { - _isBlockedCache.Clear(); - return true; + if (_firewallSet.Remove(entry)) + { + Interlocked.Increment(ref _firewallVersion); // Update version + return true; + } + return false; + } + finally + { + _firewallLock.ExitWriteLock(); } - - return false; } private class InternalValidationEntry : BaseFirewallEntry diff --git a/Projects/Server/Network/Firewall/IFirewallEntry.cs b/Projects/Server/Network/Firewall/IFirewallEntry.cs index 4a68f2441..8a09f7121 100644 --- a/Projects/Server/Network/Firewall/IFirewallEntry.cs +++ b/Projects/Server/Network/Firewall/IFirewallEntry.cs @@ -40,12 +40,12 @@ public interface IFirewallEntry : IComparable return 1; } - if (MaxIpAddress < other.MaxIpAddress) + if (MaxIpAddress > other.MaxIpAddress) { return -1; } - if (MaxIpAddress > other.MaxIpAddress) + if (MaxIpAddress < other.MaxIpAddress) { return 1; } diff --git a/Projects/Server/Network/IPLimiter.cs b/Projects/Server/Network/IPLimiter.cs deleted file mode 100644 index dc4717645..000000000 --- a/Projects/Server/Network/IPLimiter.cs +++ /dev/null @@ -1,108 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2024 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: IPLimiter.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.Generic; -using System.Net; - -namespace Server.Misc; - -public static class IPLimiter -{ - private static readonly SortedSet _connectionAttempts = []; - private static readonly SortedSet _throttledAddresses = []; - - private static readonly IPAddress _localHost = IPAddress.Parse("127.0.0.1"); - - public static TimeSpan ConnectionAttemptsDuration { get; private set; } - public static TimeSpan ConnectionThrottleDuration { get; private set; } - - public static bool Enabled { get; private set; } - public static int MaxConnections { get; private set; } - - public static void Configure() - { - Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true); - MaxConnections = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 5); - ConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10)); - ConnectionThrottleDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.connectionThrottleDuration", TimeSpan.FromMinutes(5)); - } - - private static readonly IPAccessLog _accessCheck = new(IPAddress.None, DateTime.MinValue); - - public static bool Verify(IPAddress ourAddress) - { - if (!Enabled || ourAddress.Equals(_localHost)) - { - return true; - } - - var now = Core.Now; - - IPAccessLog accessLog; - - while (_throttledAddresses.Count > 0) - { - accessLog = _throttledAddresses.Min; - if (now <= accessLog.Expiration) - { - break; - } - - _throttledAddresses.Remove(accessLog); - } - - _accessCheck.IPAddress = ourAddress; - - if (_connectionAttempts.TryGetValue(_accessCheck, out accessLog)) - { - _connectionAttempts.Remove(accessLog); - accessLog.Count++; - accessLog.Expiration = now + ConnectionAttemptsDuration; - - if (now <= accessLog.Expiration && accessLog.Count >= MaxConnections) - { - _throttledAddresses.Add(accessLog); - return false; - } - } - else - { - accessLog = new IPAccessLog(ourAddress, now + ConnectionAttemptsDuration); - } - - // Add it back so it is sorted properly - _connectionAttempts.Add(accessLog); - - return true; - } - - private class IPAccessLog : IComparable - { - public IPAddress IPAddress; - public DateTime Expiration; - public int Count; - - public IPAccessLog(IPAddress ipAddress, DateTime expiration) - { - IPAddress = ipAddress; - Expiration = expiration; - Count = 1; - } - - public int CompareTo(IPAccessLog other) => - IPAddress.Equals(other.IPAddress) ? 0 : Expiration.CompareTo(other.Expiration); - } -} diff --git a/Projects/Server/Network/IPRateLimiter.cs b/Projects/Server/Network/IPRateLimiter.cs new file mode 100644 index 000000000..5de0ad552 --- /dev/null +++ b/Projects/Server/Network/IPRateLimiter.cs @@ -0,0 +1,197 @@ +/************************************************************************* + * 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); + + int maxToProcess = Math.Min(_cleanupQueue.Count, 500); + var nowTicks = Core.TickCount; + + for (int 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; + } + } +} diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index d2c6751c8..eaf39af37 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -13,15 +13,17 @@ * along with this program. If not, see . * *************************************************************************/ +using System; +using System.Buffers; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using Server.Logging; -using Server.Misc; namespace Server.Network; @@ -35,8 +37,11 @@ public static class TcpServer public static IPEndPoint[] ListeningAddresses { get; private set; } public static Socket[] Listeners { get; private set; } + private static IPRateLimiter _ipRateLimiter; + public static void Start() { + _ipRateLimiter = new IPRateLimiter(10, 10000, 1000, 2.0, 3_600_000, Core.ClosingTokenSource.Token); HashSet listeningAddresses = []; List listeners = []; foreach (var ipep in ServerConfiguration.Listeners) @@ -123,49 +128,92 @@ public static class TcpServer return null; } - private static async void BeginAcceptingSockets(Socket listener) + private static async ValueTask BeginAcceptingSockets(Socket listener) { while (!Core.Closing) { - Socket socket = null; try { - socket = await listener.AcceptAsync(); + var socket = await listener.AcceptAsync(Core.ClosingTokenSource.Token); var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address; - if (!IPLimiter.Verify(remoteIP)) + if (!_ipRateLimiter.Verify(remoteIP, out var totalAttempts)) { - TraceDisconnect("Past IP limit threshold", remoteIP); - logger.Debug("{Address} Past IP limit threshold", remoteIP); + logger.Debug("{Address} Past IP limit threshold ({TotalAttempts})", remoteIP, totalAttempts); } else if (Firewall.IsBlocked(remoteIP)) { - TraceDisconnect("Firewalled", remoteIP); logger.Debug("{Address} Firewalled", remoteIP); } else { - var args = new SocketConnectEventArgs(socket); - EventSink.InvokeSocketConnect(args); - - if (args.AllowConnection) - { - _ = new NetState(socket); - continue; - } - - TraceDisconnect("Rejected by socket event handler", remoteIP); - - // Reject the connection - socket.Send(_socketRejected, SocketFlags.None); + _ = Task.Run(() => ProcessSocketConnection(socket), Core.ClosingTokenSource.Token); } - - CloseSocket(socket); } catch { + // ignored + } + } + } + + private static async ValueTask ProcessSocketConnection(Socket socket) + { + byte[] firstBytes = ArrayPool.Shared.Rent(83); + + var cts = CancellationTokenSource.CreateLinkedTokenSource(Core.ClosingTokenSource.Token); + cts.CancelAfter(TimeSpan.FromMilliseconds(500)); + + try + { + var bytesRead = await socket.ReceiveAsync(firstBytes, SocketFlags.Peek, cts.Token); + + var isValid = + // Older clients only send the 4 byte seed first + (UOClient.MinRequired == null || UOClient.MinRequired < ClientVersion.Version6050) && bytesRead == 4 || + (UOClient.MaxRequired == null || UOClient.MaxRequired >= ClientVersion.Version6050) && ( + // Account Login - 0xEF + 0x80 (83 bytes) + bytesRead >= 83 && firstBytes[0] == 0xEF && firstBytes[21] == 0x80 || + // Game Login - 4 bytes + 0x91 (69 bytes) + firstBytes[4] == 0x91 && bytesRead >= 69 + ); + + // TODO: Validate client version is v4 -> v7 for 0xEF packet + // TODO: Validate Account Login seed matches Game Login seed + // TODO: Validate AuthId for 0x91 packet + // TODO: Validate username is ascii and not empty + // TODO: Validate password is ascii and not empty + if (isValid) + { + var args = new SocketConnectEventArgs(socket); + EventSink.InvokeSocketConnect(args); + + if (args.AllowConnection) + { + Core.LoopContext.Post(() => _ = new NetState(socket), EventLoopContext.Priority.High); + return; + } + + logger.Debug("{Address} Rejected by socket handler", ((IPEndPoint)socket.RemoteEndPoint)!.Address); + + cts.TryReset(); + cts.CancelAfter(TimeSpan.FromMilliseconds(500)); + await socket.SendAsync(_socketRejected, SocketFlags.None, cts.Token); CloseSocket(socket); } + else + { + ForceCloseSocket(socket); + } + } + catch + { + ForceCloseSocket(socket); + } + finally + { + ArrayPool.Shared.Return(firstBytes); + cts.Dispose(); } } @@ -182,22 +230,16 @@ public static class TcpServer } } - private static void TraceDisconnect(string reason, IPAddress ip) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ForceCloseSocket(Socket socket) { try { - using StreamWriter op = new StreamWriter("network-socket-disconnects.log", true); - op.WriteLine($"# {Core.Now}"); - - op.WriteLine($"Address: {ip}"); - op.WriteLine(reason); - - op.WriteLine(); - op.WriteLine(); + socket.Disconnect(false); } - catch + finally { - // ignored + socket.Close(0); } } } diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index bfc16b87b..2fb3d72ef 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -44,5 +44,8 @@ + + <_Parameter1>Server.Tests + diff --git a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs index 479f79c8d..85f5ec732 100644 --- a/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs +++ b/Projects/UOContent.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs @@ -1,5 +1,3 @@ -using System; -using Server.Buffers; using Server.Gumps; namespace Server.Tests.Gumps; diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index 1d8a8a024..fa22ceca6 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -170,7 +170,7 @@ namespace Server.Gumps AddLabel(150, 150, LabelHue, banned.ToString()); AddLabel(20, 170, LabelHue, "Firewalled:"); - AddLabel(150, 170, LabelHue, Firewall.FirewallSet.Count.ToString()); + AddLabel(150, 170, LabelHue, Firewall.FirewallSetCount.ToString()); AddLabel(20, 190, LabelHue, "Clients:"); AddLabel(150, 190, LabelHue, NetState.Instances.Count.ToString()); @@ -1165,7 +1165,16 @@ namespace Server.Gumps { AddFirewallHeader(); - m_List ??= Firewall.FirewallSet.ToList(); + if (m_List == null) + { + Firewall.ReadFirewallSet(firewallSet => + { + list = new List(firewallSet.Count); + list.AddRange(firewallSet); + }); + + m_List = list; + } AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address"); @@ -1178,7 +1187,7 @@ namespace Server.Gumps AddImage(375, 122, 0x25EA); } - if ((listPage + 1) * 12 < m_List.Count) + if ((listPage + 1) * 12 < m_List!.Count) { AddButton(392, 122, 0x15E1, 0x15E5, GetButtonID(1, 1)); } @@ -3470,19 +3479,22 @@ namespace Server.Gumps if (string.IsNullOrEmpty(match)) { - notice = "You must enter a username to search."; + notice = "You must enter an IP to search."; } else { - foreach (var check in Firewall.FirewallSet) + Firewall.ReadFirewallSet(firewallSet => { - var checkStr = check.ToString(); - - if (checkStr.ContainsOrdinal(match)) + foreach (var check in firewallSet) { - results.Add(check); + var checkStr = check.ToString(); + + if (checkStr.ContainsOrdinal(match)) + { + results.Add(check); + } } - } + }); } if (results.Count == 1) diff --git a/Projects/UOContent/Misc/AdminFirewall.cs b/Projects/UOContent/Misc/AdminFirewall.cs index 684546fba..a4275d90c 100644 --- a/Projects/UOContent/Misc/AdminFirewall.cs +++ b/Projects/UOContent/Misc/AdminFirewall.cs @@ -127,10 +127,13 @@ public static class AdminFirewall public static void Save() { - using var op = new StreamWriter(firewallConfigPath); - foreach (var entry in Firewall.FirewallSet) + Firewall.ReadFirewallSet(firewallSet => { - op.WriteLine(entry); - } + using var op = new StreamWriter(firewallConfigPath); + foreach (var entry in firewallSet) + { + op.WriteLine(entry); + } + }); } } diff --git a/Projects/UOContent/Misc/ClientVerification.cs b/Projects/UOContent/Misc/ClientVerification.cs index 27aac5f1f..893dc80b3 100644 --- a/Projects/UOContent/Misc/ClientVerification.cs +++ b/Projects/UOContent/Misc/ClientVerification.cs @@ -27,14 +27,12 @@ namespace Server.Misc public static bool AllowKR => (AllowedClientTypes & ClientType.KR) != 0; public static bool AllowSA => (AllowedClientTypes & ClientType.SA) != 0; - public static ClientVersion MinRequired { get; private set; } - public static ClientVersion MaxRequired { get; private set; } public static TimeSpan KickDelay { get; private set; } public static void Configure() { - MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null); - MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null); + UOClient.MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null); + UOClient.MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null); _enable = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true); _invalidClientResponse = @@ -51,12 +49,12 @@ namespace Server.Misc public static void Initialize() { - if (MinRequired == null && MaxRequired == null) + if (UOClient.MinRequired == null && UOClient.MaxRequired == null) { - MinRequired = UOClient.ServerClientVersion; + UOClient.MinRequired = UOClient.ServerClientVersion; } - if (MinRequired != null || MaxRequired != null) + if (UOClient.MinRequired != null || UOClient.MaxRequired != null) { logger.Information( "Restricting client version to {ClientVersion}. Action to be taken: {Action}", @@ -70,17 +68,17 @@ namespace Server.Misc { if (_versionExpression == null) { - if (MinRequired != null && MaxRequired != null) + if (UOClient.MinRequired != null && UOClient.MaxRequired != null) { - _versionExpression = $"{MinRequired}-{MaxRequired}"; + _versionExpression = $"{UOClient.MinRequired}-{UOClient.MaxRequired}"; } - else if (MinRequired != null) + else if (UOClient.MinRequired != null) { - _versionExpression = $"{MinRequired} or newer"; + _versionExpression = $"{UOClient.MinRequired} or newer"; } else { - _versionExpression = $"{MaxRequired} or older"; + _versionExpression = $"{UOClient.MaxRequired} or older"; } } @@ -133,14 +131,14 @@ namespace Server.Misc bool shouldKick = false; bool isKRClient = version.Type == ClientType.KR; - if (!isKRClient && MinRequired != null && version < MinRequired) + if (!isKRClient && UOClient.MinRequired != null && version < UOClient.MinRequired) { - sb.Append($"This server doesn't support clients older than {MinRequired}."); + sb.Append($"This server doesn't support clients older than {UOClient.MinRequired}."); shouldKick = strictRequirement; } - else if (!isKRClient && MaxRequired != null && version > MaxRequired) + else if (!isKRClient && UOClient.MaxRequired != null && version > UOClient.MaxRequired) { - sb.Append($"This server doesn't support clients newer than {MaxRequired}."); + sb.Append($"This server doesn't support clients newer than {UOClient.MaxRequired}."); shouldKick = strictRequirement; } else diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index 3a658effe..1026c986a 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -424,7 +424,7 @@ public static class IncomingAccountPackets if (state.Seed == 0) { state.LogInfo("Invalid client detected, disconnecting"); - state.Disconnect("Duplicate seed sent."); + state.Disconnect("Invalid client detected"); return; }