fix: Fixes TCPServer accept async, makes Firewall/IP Limiter multithreaded (#2134)

This commit is contained in:
Kamron Batman 2025-02-27 22:19:38 -08:00 committed by GitHub
parent bcab91255a
commit 2a8c62e8be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 578 additions and 215 deletions

View file

@ -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));
}
}

View file

@ -30,6 +30,7 @@ public class ClientVersion : IComparable<ClientVersion>, IComparer<ClientVersion
public static readonly ClientVersion Version6000 = new("6.0.0.0"); public static readonly ClientVersion Version6000 = new("6.0.0.0");
public static readonly ClientVersion Version6000KR = new("66.55.38"); // KR 2.44.0.15 (First release) public static readonly ClientVersion Version6000KR = new("66.55.38"); // KR 2.44.0.15 (First release)
public static readonly ClientVersion Version6017 = new("6.0.1.7"); public static readonly ClientVersion Version6017 = new("6.0.1.7");
public static readonly ClientVersion Version6050 = new("6.0.5.0");
public static readonly ClientVersion Version60142 = new("6.0.14.2"); public static readonly ClientVersion Version60142 = new("6.0.14.2");
public static readonly ClientVersion Version60142KR = new("66.55.53"); // KR 2.59.0.2 public static readonly ClientVersion Version60142KR = new("66.55.53"); // KR 2.59.0.2
public static readonly ClientVersion Version7000 = new("7.0.0.0"); public static readonly ClientVersion Version7000 = new("7.0.0.0");

View file

@ -30,6 +30,8 @@ public static class UOClient
public static CUOSettings CuoSettings { get; private set; } public static CUOSettings CuoSettings { get; private set; }
public static ClientVersion ServerClientVersion { get; private set; } public static ClientVersion ServerClientVersion { get; private set; }
public static ClientVersion MinRequired { get; set; }
public static ClientVersion MaxRequired { get; set; }
public static void Load() public static void Load()
{ {

View file

@ -21,18 +21,29 @@ namespace Server;
public sealed class EventLoopContext : SynchronizationContext public sealed class EventLoopContext : SynchronizationContext
{ {
private readonly ConcurrentQueue<Action> _queue; public enum Priority
private readonly Thread _mainThread;
public EventLoopContext()
{ {
_queue = new ConcurrentQueue<Action>(); Normal,
High
}
private readonly ConcurrentQueue<Action> _queue;
private readonly ConcurrentQueue<Action> _priorityQueue;
private readonly Thread _mainThread;
private readonly int _maxPerFrame;
public EventLoopContext(int maxPerFrame = 128)
{
_maxPerFrame = maxPerFrame;
_queue = [];
_priorityQueue = [];
_mainThread = Thread.CurrentThread; _mainThread = Thread.CurrentThread;
} }
public override SynchronizationContext CreateCopy() => new EventLoopContext(); 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)); 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!"); 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++) for (int i = 0; i < count; i++)
{ {

View file

@ -1,6 +1,6 @@
/************************************************************************* /*************************************************************************
* ModernUO * * ModernUO *
* Copyright 2019-2024 - ModernUO Development Team * * Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com * * Email: hi@modernuo.com *
* File: Firewall.cs * * File: Firewall.cs *
* * * *
@ -14,28 +14,44 @@
*************************************************************************/ *************************************************************************/
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Threading;
namespace Server.Network; namespace Server.Network;
public static class Firewall public static class Firewall
{ {
[ThreadStatic]
private static InternalValidationEntry _validationEntry; private static InternalValidationEntry _validationEntry;
private static readonly Dictionary<IPAddress, bool> _isBlockedCache = new(); private static readonly ConcurrentDictionary<IPAddress, int> _isBlockedCache = [];
private static readonly ReaderWriterLockSlim _firewallLock = new(LockRecursionPolicy.NoRecursion);
private static readonly SortedSet<IFirewallEntry> _firewallSet = new(); private static int _firewallVersion;
private static readonly SortedSet<IFirewallEntry> _firewallSet = [];
public static SortedSet<IFirewallEntry> FirewallSet => _firewallSet; public static int FirewallSetCount => _firewallSet.Count;
public static void ReadFirewallSet(Action<IReadOnlySet<IFirewallEntry>> callback)
{
_firewallLock.EnterReadLock();
try
{
callback(_firewallSet);
}
finally
{
_firewallLock.ExitReadLock();
}
}
internal static bool IsBlocked(IPAddress address) internal static bool IsBlocked(IPAddress address)
{ {
ref var isBlocked = ref CollectionsMarshal.GetValueRefOrAddDefault(_isBlockedCache, address, out var exists); if (_isBlockedCache.TryGetValue(address, out var blockVersion) && blockVersion == _firewallVersion)
if (exists)
{ {
return isBlocked; return true;
} }
if (_validationEntry == null) if (_validationEntry == null)
@ -47,34 +63,68 @@ public static class Firewall
_validationEntry.Address = address; _validationEntry.Address = address;
} }
// Get all entries that are lower than our validation entry if (CheckBlocked(_validationEntry))
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 (firewallEntry.IsBlocked(_validationEntry.MinIpAddress)) _isBlockedCache[address] = _firewallVersion;
{ return true;
isBlocked = true;
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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Add(IFirewallEntry firewallEntry) public static bool Add(IFirewallEntry firewallEntry)
{ {
if (_firewallSet.Add(firewallEntry)) _firewallLock.EnterWriteLock();
try
{ {
_isBlockedCache.Clear(); if (_firewallSet.Add(firewallEntry))
return true; {
Interlocked.Increment(ref _firewallVersion); // Update version
return true;
}
return false;
}
finally
{
_firewallLock.ExitWriteLock();
} }
return false;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -85,13 +135,20 @@ public static class Firewall
return false; return false;
} }
if (_firewallSet.Remove(entry)) _firewallLock.EnterWriteLock();
try
{ {
_isBlockedCache.Clear(); if (_firewallSet.Remove(entry))
return true; {
Interlocked.Increment(ref _firewallVersion); // Update version
return true;
}
return false;
}
finally
{
_firewallLock.ExitWriteLock();
} }
return false;
} }
private class InternalValidationEntry : BaseFirewallEntry private class InternalValidationEntry : BaseFirewallEntry

View file

@ -40,12 +40,12 @@ public interface IFirewallEntry : IComparable<IFirewallEntry>
return 1; return 1;
} }
if (MaxIpAddress < other.MaxIpAddress) if (MaxIpAddress > other.MaxIpAddress)
{ {
return -1; return -1;
} }
if (MaxIpAddress > other.MaxIpAddress) if (MaxIpAddress < other.MaxIpAddress)
{ {
return 1; return 1;
} }

View file

@ -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 <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.Net;
namespace Server.Misc;
public static class IPLimiter
{
private static readonly SortedSet<IPAccessLog> _connectionAttempts = [];
private static readonly SortedSet<IPAccessLog> _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<IPAccessLog>
{
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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
*************************************************************************/
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<IPStats> _statsPool = [];
private const int MaxPoolSize = 32_768;
private readonly SemaphoreSlim _cleanupSignal = new(0, 1);
private readonly ConcurrentDictionary<IPAddress, IPStats> _ipAttempts;
private readonly ConcurrentQueue<IPAddress> _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;
}
}
}

View file

@ -13,15 +13,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * * along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/ *************************************************************************/
using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Server.Logging; using Server.Logging;
using Server.Misc;
namespace Server.Network; namespace Server.Network;
@ -35,8 +37,11 @@ public static class TcpServer
public static IPEndPoint[] ListeningAddresses { get; private set; } public static IPEndPoint[] ListeningAddresses { get; private set; }
public static Socket[] Listeners { get; private set; } public static Socket[] Listeners { get; private set; }
private static IPRateLimiter _ipRateLimiter;
public static void Start() public static void Start()
{ {
_ipRateLimiter = new IPRateLimiter(10, 10000, 1000, 2.0, 3_600_000, Core.ClosingTokenSource.Token);
HashSet<IPEndPoint> listeningAddresses = []; HashSet<IPEndPoint> listeningAddresses = [];
List<Socket> listeners = []; List<Socket> listeners = [];
foreach (var ipep in ServerConfiguration.Listeners) foreach (var ipep in ServerConfiguration.Listeners)
@ -123,49 +128,92 @@ public static class TcpServer
return null; return null;
} }
private static async void BeginAcceptingSockets(Socket listener) private static async ValueTask BeginAcceptingSockets(Socket listener)
{ {
while (!Core.Closing) while (!Core.Closing)
{ {
Socket socket = null;
try try
{ {
socket = await listener.AcceptAsync(); var socket = await listener.AcceptAsync(Core.ClosingTokenSource.Token);
var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address; 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 ({TotalAttempts})", remoteIP, totalAttempts);
logger.Debug("{Address} Past IP limit threshold", remoteIP);
} }
else if (Firewall.IsBlocked(remoteIP)) else if (Firewall.IsBlocked(remoteIP))
{ {
TraceDisconnect("Firewalled", remoteIP);
logger.Debug("{Address} Firewalled", remoteIP); logger.Debug("{Address} Firewalled", remoteIP);
} }
else else
{ {
var args = new SocketConnectEventArgs(socket); _ = Task.Run(() => ProcessSocketConnection(socket), Core.ClosingTokenSource.Token);
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);
} }
CloseSocket(socket);
} }
catch catch
{ {
// ignored
}
}
}
private static async ValueTask ProcessSocketConnection(Socket socket)
{
byte[] firstBytes = ArrayPool<byte>.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); CloseSocket(socket);
} }
else
{
ForceCloseSocket(socket);
}
}
catch
{
ForceCloseSocket(socket);
}
finally
{
ArrayPool<byte>.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 try
{ {
using StreamWriter op = new StreamWriter("network-socket-disconnects.log", true); socket.Disconnect(false);
op.WriteLine($"# {Core.Now}");
op.WriteLine($"Address: {ip}");
op.WriteLine(reason);
op.WriteLine();
op.WriteLine();
} }
catch finally
{ {
// ignored socket.Close(0);
} }
} }
} }

View file

@ -44,5 +44,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="Migrations/*.v*.json" /> <AdditionalFiles Include="Migrations/*.v*.json" />
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Server.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,5 +1,3 @@
using System;
using Server.Buffers;
using Server.Gumps; using Server.Gumps;
namespace Server.Tests.Gumps; namespace Server.Tests.Gumps;

View file

@ -170,7 +170,7 @@ namespace Server.Gumps
AddLabel(150, 150, LabelHue, banned.ToString()); AddLabel(150, 150, LabelHue, banned.ToString());
AddLabel(20, 170, LabelHue, "Firewalled:"); 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(20, 190, LabelHue, "Clients:");
AddLabel(150, 190, LabelHue, NetState.Instances.Count.ToString()); AddLabel(150, 190, LabelHue, NetState.Instances.Count.ToString());
@ -1165,7 +1165,16 @@ namespace Server.Gumps
{ {
AddFirewallHeader(); AddFirewallHeader();
m_List ??= Firewall.FirewallSet.ToList<object>(); if (m_List == null)
{
Firewall.ReadFirewallSet(firewallSet =>
{
list = new List<object>(firewallSet.Count);
list.AddRange(firewallSet);
});
m_List = list;
}
AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address"); AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address");
@ -1178,7 +1187,7 @@ namespace Server.Gumps
AddImage(375, 122, 0x25EA); 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)); AddButton(392, 122, 0x15E1, 0x15E5, GetButtonID(1, 1));
} }
@ -3470,19 +3479,22 @@ namespace Server.Gumps
if (string.IsNullOrEmpty(match)) if (string.IsNullOrEmpty(match))
{ {
notice = "You must enter a username to search."; notice = "You must enter an IP to search.";
} }
else else
{ {
foreach (var check in Firewall.FirewallSet) Firewall.ReadFirewallSet(firewallSet =>
{ {
var checkStr = check.ToString(); foreach (var check in firewallSet)
if (checkStr.ContainsOrdinal(match))
{ {
results.Add(check); var checkStr = check.ToString();
if (checkStr.ContainsOrdinal(match))
{
results.Add(check);
}
} }
} });
} }
if (results.Count == 1) if (results.Count == 1)

View file

@ -127,10 +127,13 @@ public static class AdminFirewall
public static void Save() public static void Save()
{ {
using var op = new StreamWriter(firewallConfigPath); Firewall.ReadFirewallSet(firewallSet =>
foreach (var entry in Firewall.FirewallSet)
{ {
op.WriteLine(entry); using var op = new StreamWriter(firewallConfigPath);
} foreach (var entry in firewallSet)
{
op.WriteLine(entry);
}
});
} }
} }

View file

@ -27,14 +27,12 @@ namespace Server.Misc
public static bool AllowKR => (AllowedClientTypes & ClientType.KR) != 0; public static bool AllowKR => (AllowedClientTypes & ClientType.KR) != 0;
public static bool AllowSA => (AllowedClientTypes & ClientType.SA) != 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 TimeSpan KickDelay { get; private set; }
public static void Configure() public static void Configure()
{ {
MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null); UOClient.MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null);
MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null); UOClient.MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null);
_enable = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true); _enable = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true);
_invalidClientResponse = _invalidClientResponse =
@ -51,12 +49,12 @@ namespace Server.Misc
public static void Initialize() 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( logger.Information(
"Restricting client version to {ClientVersion}. Action to be taken: {Action}", "Restricting client version to {ClientVersion}. Action to be taken: {Action}",
@ -70,17 +68,17 @@ namespace Server.Misc
{ {
if (_versionExpression == null) 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 else
{ {
_versionExpression = $"{MaxRequired} or older"; _versionExpression = $"{UOClient.MaxRequired} or older";
} }
} }
@ -133,14 +131,14 @@ namespace Server.Misc
bool shouldKick = false; bool shouldKick = false;
bool isKRClient = version.Type == ClientType.KR; 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; 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; shouldKick = strictRequirement;
} }
else else

View file

@ -424,7 +424,7 @@ public static class IncomingAccountPackets
if (state.Seed == 0) if (state.Seed == 0)
{ {
state.LogInfo("Invalid client detected, disconnecting"); state.LogInfo("Invalid client detected, disconnecting");
state.Disconnect("Duplicate seed sent."); state.Disconnect("Invalid client detected");
return; return;
} }