fix: Fixes TCPServer accept async, makes Firewall/IP Limiter multithreaded (#2134)
This commit is contained in:
parent
bcab91255a
commit
2a8c62e8be
15 changed files with 578 additions and 215 deletions
137
Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs
Normal file
137
Projects/Server.Tests/Tests/Network/Firewall/FirewallTests.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
197
Projects/Server/Network/IPRateLimiter.cs
Normal file
197
Projects/Server/Network/IPRateLimiter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
using System;
|
|
||||||
using Server.Buffers;
|
|
||||||
using Server.Gumps;
|
using Server.Gumps;
|
||||||
|
|
||||||
namespace Server.Tests.Gumps;
|
namespace Server.Tests.Gumps;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue