feat: Moves TcpServer to another thread. Rewrites Firewall (#1660)

## Breaking Changes
* The Firewall and IP Limiter have been rewritten. Please read the notes carefully!
* `TcpServer.Instances` moved back to `NetState.Instances` - sorry - it was stupid to move it to begin with.

> [!Note]
> Sockets that fail the IP Limiter or Firewall will be immediately and forcibly disconnected.
> This means they will be stuck at "Verifying account..." if it was a real client.

### Summary
- Removes firewall wildcard support.
- Removes `AccessRestrictions`.
- Moves Firewall/IPLimiter to the core.
- Moves `TcpServer` to its own thread.
- Removes the `SocketConnect` and `SocketDisconnect` event sinks.
- Moves `Instances` back to `NetState.Instances`.
- Fixes a long standing bug with bad handling of duplicate listener addresses.

#### Firewall
The firewall has been completely rewritten. There is now an "Admin Firewall" which saves to the config file. Secondarily, there is an internal firewall used exclusively by the TcpServer while processing sockets. The Admin firewall mirrors it's additions/deletions to the internal firewall by adding requests to a queue.

> [!IMPORTANT]  
> **Wildcard firewall entries, such as `X`, `*`, `?` are not allowed.**
> **Ranges in between IP classes or sextets are not allowed.**
> **Please make sure to use one of the following:**
> * IP Address - `192.168.1.1`
> * CIDR - `192.168.1.0/24`
> * Range - `192.168.1.1-192.168.1.100`

#### IP Limiter
The IP Limiter has been completely rewritten. The available configurations are:
```json
"ipLimiter.enable": "True",
"ipLimiter.maxConnectionsPerIP": 10,
"ipLimiter.clearConnectionAttemptsDuration": "00:00:00:10",
"ipLimiter.clearThrottledDuration": "00:00:02:00",
```

The IP Limiter is set up to prevent spamming connections from the same IP. Every time an IP connects, it is added to a connection list. After 10 attempts, the IP is added to the throttle list. To keep the system fast, the connection list is entirely wiped every 10 seconds, and the throttle list is entirely wiped every 2 minutes.
This commit is contained in:
Kamron Batman 2024-01-20 14:25:12 -08:00 committed by GitHub
parent 5f3de6537b
commit 4cd668ef61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1117 additions and 1030 deletions

View file

@ -564,7 +564,6 @@ public static class Core
Timer.Slice(_tickCount);
// Handle networking
TcpServer.Slice();
NetState.Slice();
PingServer.Slice();

View file

@ -0,0 +1,71 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: BaseFirewallEntry.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.Net;
using System.Runtime.CompilerServices;
namespace Server.Network;
public abstract class BaseFirewallEntry : IFirewallEntry, ISpanFormattable
{
public abstract UInt128 MinIpAddress { get; }
public abstract UInt128 MaxIpAddress { get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsBlocked(IPAddress address) => IsBlocked(address.ToUInt128());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsBlocked(UInt128 address) => address >= MinIpAddress && address <= MaxIpAddress;
public override string ToString() =>
MinIpAddress == MaxIpAddress ? MinIpAddress.ToIpAddress().ToString()
: $"{MinIpAddress.ToIpAddress()}-{MaxIpAddress.ToIpAddress()}";
public string ToString(string? format, IFormatProvider? formatProvider) =>
// format and provider are explicitly ignored
ToString();
public bool TryFormat(
Span<char> destination,
out int charsWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider
)
{
if (!((ISpanFormattable)MinIpAddress.ToIpAddress()).TryFormat(destination, out charsWritten, format, provider))
{
return false;
}
if (MinIpAddress == MaxIpAddress)
{
return true;
}
// Range
destination[charsWritten++] = '-';
var total = charsWritten;
if (!((ISpanFormattable)MaxIpAddress.ToIpAddress()).TryFormat(destination[charsWritten..], out charsWritten, format, provider))
{
return false;
}
charsWritten += total;
return true;
}
}

View file

@ -0,0 +1,75 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: CidrFirewallEntry.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.Net;
using System.Net.Sockets;
namespace Server.Network;
public class CidrFirewallEntry : BaseFirewallEntry
{
public override UInt128 MinIpAddress { get; }
public override UInt128 MaxIpAddress { get; }
public CidrFirewallEntry(string ipAddressOrCidr)
: this(ParseIPAddress(ipAddressOrCidr, out var prefixLength), prefixLength)
{
}
public CidrFirewallEntry(IPAddress minAddress, IPAddress maxAddress)
{
MinIpAddress = minAddress.ToUInt128();
MaxIpAddress = maxAddress.ToUInt128();
}
public CidrFirewallEntry(IPAddress ipAddress, int prefixLength)
{
Span<byte> bytes = stackalloc byte[16];
if (ipAddress.AddressFamily != AddressFamily.InterNetworkV6)
{
prefixLength += 96; // 32 -> 128
}
ipAddress.WriteMappedIPv6To(bytes);
MinIpAddress = Utility.CreateCidrAddress(bytes, prefixLength, false);
MaxIpAddress = Utility.CreateCidrAddress(bytes, prefixLength, true);
}
private static IPAddress ParseIPAddress(ReadOnlySpan<char> ipString, out int prefixLength)
{
int slashIndex = ipString.IndexOf('/');
var ipAddress = IPAddress.Parse(slashIndex > -1 ? ipString[..slashIndex] : ipString);
var maxPrefixLength = ipAddress.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
if (slashIndex == -1)
{
prefixLength = maxPrefixLength;
}
else
{
var prefixPart = ipString[(slashIndex + 1)..];
if (!int.TryParse(prefixPart, out prefixLength) || prefixLength < 0 || prefixLength > maxPrefixLength)
{
throw new ArgumentException("Invalid prefix length.");
}
}
return ipAddress;
}
}

View file

@ -0,0 +1,160 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Firewall.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.Collections.Generic;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Server.Logging;
namespace Server.Network;
public static class Firewall
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Firewall));
private static InternalValidationEntry _validationEntry;
private static readonly Dictionary<IPAddress, bool> _isBlockedCache = new();
private static readonly ConcurrentQueue<(IFirewallEntry FirewallyEntry, bool Remove)> _firewallQueue = new();
private static readonly SortedSet<IFirewallEntry> _firewallSet = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IFirewallEntry RequestAddSingleIPEntry(string entry)
{
try
{
var firewallEntry = new SingleIpFirewallEntry(entry);
_firewallQueue.Enqueue((firewallEntry, false));
return firewallEntry;
}
catch (Exception e)
{
logger.Warning(e, "Failed to add firewall entry: {Pattern}", entry);
return null;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IFirewallEntry RequestAddCIDREntry(string entry)
{
try
{
var firewallEntry = new CidrFirewallEntry(entry);
_firewallQueue.Enqueue((firewallEntry, false));
return firewallEntry;
}
catch (Exception e)
{
logger.Warning(e, "Failed to add firewall entry: {Pattern}", entry);
return null;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RequestAddEntry(IFirewallEntry entry)
{
_firewallQueue.Enqueue((entry, false));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RequestRemoveEntry(IFirewallEntry entry)
{
_firewallQueue.Enqueue((entry, true));
}
internal static void ProcessQueue()
{
while (_firewallQueue.TryDequeue(out var entry))
{
if (entry.Remove)
{
RemoveEntry(entry.FirewallyEntry);
}
else
{
AddEntry(entry.FirewallyEntry);
}
}
}
internal static bool IsBlocked(IPAddress address)
{
ref var isBlocked = ref CollectionsMarshal.GetValueRefOrAddDefault(_isBlockedCache, address, out var exists);
if (exists)
{
return isBlocked;
}
if (_validationEntry == null)
{
_validationEntry = new InternalValidationEntry(address);
}
else
{
_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 (firewallEntry.IsBlocked(_validationEntry.MinIpAddress))
{
isBlocked = true;
return true;
}
}
isBlocked = view.Max?.IsBlocked(_validationEntry.MinIpAddress) == true;
return isBlocked;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddEntry(IFirewallEntry firewallEntry)
{
_firewallSet.Add(firewallEntry);
_isBlockedCache.Clear();
}
private static void RemoveEntry(IFirewallEntry entry)
{
if (entry != null)
{
_firewallSet.Remove(entry);
_isBlockedCache.Clear();
}
}
private class InternalValidationEntry : BaseFirewallEntry
{
private UInt128 _address;
public IPAddress Address
{
set => _address = value.ToUInt128();
}
public override UInt128 MinIpAddress => _address;
public override UInt128 MaxIpAddress => _address;
public InternalValidationEntry(IPAddress ipAddress) => Address = ipAddress;
}
}

View file

@ -0,0 +1,59 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: IFirewallEntry.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.Net;
namespace Server.Network;
public interface IFirewallEntry : IComparable<IFirewallEntry>
{
UInt128 MinIpAddress { get; }
UInt128 MaxIpAddress { get; }
int IComparable<IFirewallEntry>.CompareTo(IFirewallEntry? other)
{
if (other == null)
{
return 1;
}
if (MinIpAddress < other.MinIpAddress)
{
return -1;
}
if (MinIpAddress > other.MinIpAddress)
{
return 1;
}
if (MaxIpAddress < other.MaxIpAddress)
{
return -1;
}
if (MaxIpAddress > other.MaxIpAddress)
{
return 1;
}
return 0; // Equal ranges
}
bool IsBlocked(IPAddress address);
bool IsBlocked(UInt128 address);
}

View file

@ -1,8 +1,8 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SocketConnectionEvent.cs *
* File: SingleIpFirewallEntry.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 *
@ -14,28 +14,17 @@
*************************************************************************/
using System;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Net;
namespace Server;
namespace Server.Network;
public class SocketConnectEventArgs
public class SingleIpFirewallEntry : BaseFirewallEntry
{
public SocketConnectEventArgs(Socket c)
{
Connection = c;
AllowConnection = true;
}
public override UInt128 MinIpAddress { get; }
public Socket Connection { get; }
public override UInt128 MaxIpAddress => MinIpAddress;
public bool AllowConnection { get; set; }
}
public static partial class EventSink
{
public static event Action<SocketConnectEventArgs> SocketConnect;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeSocketConnect(SocketConnectEventArgs e) => SocketConnect?.Invoke(e);
public SingleIpFirewallEntry(string ipAddress) => MinIpAddress = IPAddress.Parse(ipAddress).ToUInt128();
public SingleIpFirewallEntry(IPAddress ipAddress) => MinIpAddress = ipAddress.ToUInt128();
}

View file

@ -0,0 +1,115 @@
/*************************************************************************
* 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;
using System.Runtime.InteropServices;
namespace Server.Misc;
public static class IPLimiter
{
private static readonly Dictionary<IPAddress, int> _connectionAttempts = new(128);
private static readonly HashSet<IPAddress> _throttledAddresses = new();
private static long _lastClearedThrottles;
private static long _lastClearedAttempts;
public static readonly IPAddress[] Exemptions =
{
IPAddress.Parse( "127.0.0.1" )
};
public static TimeSpan ClearConnectionAttemptsDuration { get; private set; }
public static TimeSpan ClearThrottledDuration { get; private set; }
public static bool Enabled { get; private set; }
public static int MaxAddresses { get; private set; }
public static void Configure()
{
Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true);
MaxAddresses = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 10);
ClearConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10));
ClearThrottledDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearThrottledDuration", TimeSpan.FromMinutes(2));
}
public static bool IsExempt(IPAddress ip)
{
for (int i = 0; i < Exemptions.Length; i++)
{
if (ip.Equals(Exemptions[i]))
{
return true;
}
}
return false;
}
public static bool Verify(IPAddress ourAddress)
{
if (!Enabled || IsExempt(ourAddress))
{
return true;
}
var now = Core.TickCount;
if (_throttledAddresses.Count > 0)
{
if (now - _lastClearedThrottles > ClearThrottledDuration.TotalMilliseconds)
{
_lastClearedThrottles = now;
ClearThrottledAddresses();
}
else if (_throttledAddresses.Contains(ourAddress))
{
return false;
}
}
if (_connectionAttempts.Count > 0 && now - _lastClearedAttempts > ClearConnectionAttemptsDuration.TotalMilliseconds)
{
_lastClearedAttempts = now;
ClearConnectionAttempts();
}
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_connectionAttempts, ourAddress, out _);
count++;
if (count > MaxAddresses)
{
_connectionAttempts.Remove(ourAddress);
_throttledAddresses.Add(ourAddress);
return false;
}
return true;
}
private static void ClearThrottledAddresses()
{
_throttledAddresses.Clear();
}
private static void ClearConnectionAttempts()
{
_connectionAttempts.Clear();
_connectionAttempts.TrimExcess(128);
}
}

View file

@ -30,7 +30,7 @@ public static class DumpNetStates
file.WriteLine("NetState, RecvTask, SendTask, ProtocolState, ParserState");
foreach (var ns in TcpServer.Instances)
foreach (var ns in NetState.Instances)
{
file.WriteLine($"{ns}, {ns._protocolState}, {ns._parserState}");
}

View file

@ -15,6 +15,7 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
@ -53,12 +54,15 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
private static readonly IPollGroup _pollGroup = PollGroup.Create();
private static readonly Queue<NetState> _flushPending = new(2048);
private static readonly Queue<NetState> _flushedPartials = new(256);
private static readonly Queue<NetState> _disposed = new(256);
private static readonly ConcurrentQueue<NetState> _disposed = new();
private static readonly Queue<NetState> _throttled = new(256);
private static readonly Queue<NetState> _throttledPending = new(256);
public static NetStateCreatedCallback CreatedCallback { get; set; }
private static readonly HashSet<NetState> _instances = new(2048);
public static IReadOnlySet<NetState> Instances => _instances;
private readonly string _toString;
private ClientVersion _version;
private long _nextActivityCheck;
@ -152,8 +156,6 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
TraceException(ex);
Disconnect("Unable to add socket to poll group");
}
CreatedCallback?.Invoke(this);
}
// Sectors
@ -965,6 +967,16 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
public static void Slice()
{
const int maxEntriesPerLoop = 32;
var count = 0;
while (++count <= maxEntriesPerLoop && TcpServer.ConnectedQueue.TryDequeue(out var ns))
{
CreatedCallback?.Invoke(ns);
_instances.Add(ns);
ns.LogInfo($"Connected. [{Instances.Count} Online]");
}
while (_throttled.Count > 0)
{
var ns = _throttled.Dequeue();
@ -980,7 +992,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
_throttled.Enqueue(_throttledPending.Dequeue());
}
int count = _pollGroup.Poll(_polledStates);
count = _pollGroup.Poll(_polledStates);
if (count > 0)
{
@ -1037,7 +1049,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
{
long curTicks = Core.TickCount;
foreach (var ns in TcpServer.Instances)
foreach (var ns in Instances)
{
ns.CheckAlive(curTicks);
}
@ -1116,7 +1128,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
public static void TraceDisconnect(string reason, string ip)
{
if (reason == string.Empty)
if (string.IsNullOrWhiteSpace(reason))
{
return;
}
@ -1140,6 +1152,12 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
private void Dispose()
{
// It's possible we could queue for dispose multiple times
if (Connection == null)
{
return;
}
TraceDisconnect(_disconnectReason, _toString);
if (_running)
@ -1164,7 +1182,8 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
m.NetState = null;
}
TcpServer.Instances.Remove(this);
_instances.Remove(this);
try
{
_pollGroup.Remove(Connection, _handle);
@ -1191,7 +1210,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
CityInfo = null;
Connection = null;
var count = TcpServer.Instances.Count;
var count = Instances.Count;
LogInfo(a != null ? $"Disconnected. [{count} Online] [{a}]" : $"Disconnected. [{count} Online]");
}

View file

@ -1,7 +1,23 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: PingServer.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.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Server.Logging;
namespace Server.Network;
@ -10,14 +26,11 @@ public static class PingServer
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(PingServer));
private const int MaxConnectionsPerLoop = 250;
private const int MaxConnectionsPerLoop = 128;
private const long _listenerErrorMessageDelay = 10000; // 10 seconds
private static long _nextMaximumSocketsReachedMessage;
public static int MaxQueued { get; set; }
public static int MaxConnections { get; set; }
private static ConcurrentQueue<(UdpClient, UdpReceiveResult)> _udpResponseQueue = new();
private static readonly ConcurrentQueue<(UdpClient, UdpReceiveResult)> _udpResponseQueue = new();
public static UdpClient[] Listeners { get; private set; }
@ -29,7 +42,7 @@ public static class PingServer
{
Enabled = ServerConfiguration.GetOrUpdateSetting("pingServer.enabled", true);
Port = ServerConfiguration.GetSetting("pingServer.port", 12000);
MaxConnections = ServerConfiguration.GetSetting("pingServer.maxConnections", 2048);
MaxQueued = ServerConfiguration.GetSetting("pingServer.maxConnections", 2048);
}
public static void Start()
@ -44,6 +57,7 @@ public static class PingServer
foreach (var serverIpep in ServerConfiguration.Listeners)
{
var cancellationToken = Core.ClosingTokenSource.Token;
var ipep = new IPEndPoint(serverIpep.Address, Port);
var listener = CreateListener(ipep);
@ -62,7 +76,7 @@ public static class PingServer
}
listeners.Add(listener);
BeginAcceptingSockets(listener);
Task.Run(() => BeginAcceptingUdpRequest(listener), cancellationToken).ConfigureAwait(false);
}
foreach (var ipep in listeningAddresses)
@ -126,36 +140,21 @@ public static class PingServer
return null;
}
private static async void BeginAcceptingSockets(UdpClient listener)
private static async void BeginAcceptingUdpRequest(UdpClient listener)
{
while (true)
{
if (!Enabled || Core.Closing)
{
return;
}
var cancellationToken = Core.ClosingTokenSource.Token;
while (!cancellationToken.IsCancellationRequested)
{
try
{
var result = await listener.ReceiveAsync(Core.ClosingTokenSource.Token);
var result = await listener.ReceiveAsync(cancellationToken);
if (_udpResponseQueue.Count >= MaxConnections)
if (_udpResponseQueue.Count < MaxQueued)
{
var ticks = Core.TickCount;
if (ticks - _nextMaximumSocketsReachedMessage > 0)
{
if (listener.Client.RemoteEndPoint is IPEndPoint ipep)
{
var ip = ipep.Address.ToString();
logger.Warning("Ping Listener {Address}: Failed (Maximum connections reached)", ip);
}
_nextMaximumSocketsReachedMessage = ticks + _listenerErrorMessageDelay;
}
_udpResponseQueue.Enqueue((listener, result));
}
_udpResponseQueue.Enqueue((listener, result));
}
catch
{
@ -164,7 +163,7 @@ public static class PingServer
}
}
private static async void SendResponse(UdpClient listener, byte[] data, IPEndPoint ipep)
private static async Task SendResponse(UdpClient listener, byte[] data, IPEndPoint ipep)
{
try
{

View file

@ -13,13 +13,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Concurrent;
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;
@ -27,22 +33,24 @@ public static class TcpServer
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(TcpServer));
private const int MaxConnectionsPerLoop = 250;
private const long MaximumSocketIdleDelay = 2000; // 2 seconds
private const long ListenerErrorMessageDelay = 10000; // 10 seconds
private static long _nextMaximumSocketsReachedMessage;
private static readonly SemaphoreSlim _queueSemaphore = new(0);
private static readonly ConcurrentQueue<Socket> _connectingQueue = [];
private static Thread _processConnectionsThread;
// Sanity. 256 * 1024 * 4096 = ~1.3GB of ram
public static int MaxConnections { get; set; }
private const long _listenerErrorMessageDelay = 10000; // 10 seconds
private static long _nextMaximumSocketsReachedMessage;
// AccountLoginReject BadComm
private static readonly byte[] _socketRejected = { 0x82, 0xFF };
public static IPEndPoint[] ListeningAddresses { get; private set; }
public static Socket[] Listeners { get; private set; }
public static HashSet<NetState> Instances { get; } = new(2048);
private static readonly ConcurrentQueue<NetState> _connectedQueue = new();
// By default should sort T1 then T2
public static readonly SortedSet<(long ConnectedAt, NetState NetState)> _socketsConnecting = [];
public static ConcurrentQueue<NetState> ConnectedQueue { get; } = [];
public static void Configure()
{
@ -51,45 +59,8 @@ public static class TcpServer
public static void Start()
{
HashSet<IPEndPoint> listeningAddresses = new HashSet<IPEndPoint>();
List<Socket> listeners = new List<Socket>();
foreach (var ipep in ServerConfiguration.Listeners)
{
var listener = CreateListener(ipep);
if (listener == null)
{
continue;
}
if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any))
{
listeningAddresses.UnionWith(GetListeningAddresses(ipep));
}
else
{
listeningAddresses.Add(ipep);
}
listeners.Add(listener);
BeginAcceptingSockets(listener);
}
foreach (var ipep in listeningAddresses)
{
logger.Information("Listening: {Address}:{Port}", ipep.Address, ipep.Port);
}
ListeningAddresses = listeningAddresses.ToArray();
Listeners = listeners.ToArray();
}
public static void Shutdown()
{
foreach (var listener in Listeners)
{
listener.Close();
}
_processConnectionsThread = new Thread(ProcessConnections);
_processConnectionsThread.Start();
}
public static IEnumerable<IPEndPoint> GetListeningAddresses(IPEndPoint ipep) =>
@ -114,7 +85,7 @@ public static class TcpServer
try
{
listener.Bind(ipep);
listener.Listen(32);
listener.Listen(256);
return listener;
}
catch (SocketException se)
@ -138,74 +109,201 @@ public static class TcpServer
return null;
}
public static void Slice()
private static async Task BeginAcceptingSockets(Socket listener)
{
int count = 0;
var cancellationToken = Core.ClosingTokenSource.Token;
while (++count <= MaxConnectionsPerLoop && _connectedQueue.TryDequeue(out var ns))
try
{
Instances.Add(ns);
ns.LogInfo($"Connected. [{Instances.Count} Online]");
var socket = await listener.AcceptAsync(cancellationToken);
_connectingQueue.Enqueue(socket);
_queueSemaphore.Release();
}
catch(OperationCanceledException)
{
return;
}
if (cancellationToken.IsCancellationRequested)
{
listener.Close();
return;
}
Task.Run(() => BeginAcceptingSockets(listener), cancellationToken).ConfigureAwait(false);
}
private static async void BeginAcceptingSockets(Socket listener)
private static void ProcessConnections()
{
var cancellationToken = Core.ClosingTokenSource.Token;
HashSet<IPEndPoint> listeningAddresses = [];
List<Socket> listeners = [];
foreach (var ipep in ServerConfiguration.Listeners)
{
var listener = CreateListener(ipep);
if (listener == null)
{
continue;
}
bool added;
if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any))
{
var beforeCount = listeningAddresses.Count;
listeningAddresses.UnionWith(GetListeningAddresses(ipep));
added = listeningAddresses.Count > beforeCount;
}
else
{
added = listeningAddresses.Add(ipep);
}
if (added)
{
listeners.Add(listener);
Task.Run(() => BeginAcceptingSockets(listener), cancellationToken).ConfigureAwait(false);
}
}
foreach (var ipep in listeningAddresses)
{
logger.Information("Listening: {Address}:{Port}", ipep.Address, ipep.Port);
}
ListeningAddresses = listeningAddresses.ToArray();
Listeners = listeners.ToArray();
while (true)
{
try
{
var socket = await listener.AcceptAsync();
var rejected = false;
if (Instances.Count >= MaxConnections)
while (!cancellationToken.IsCancellationRequested)
{
rejected = true;
_queueSemaphore.Wait(cancellationToken);
var ticks = Core.TickCount;
Firewall.ProcessQueue();
if (ticks - _nextMaximumSocketsReachedMessage > 0)
if (_connectingQueue.TryDequeue(out var socket))
{
if (socket.RemoteEndPoint is IPEndPoint ipep)
{
var ip = ipep.Address.ToString();
logger.Warning("Listener {Address}: Failed (Maximum connections reached)", ip);
NetState.TraceDisconnect("Maximum connections reached.", ip);
}
_nextMaximumSocketsReachedMessage = ticks + _listenerErrorMessageDelay;
ProcessConnection(socket);
}
}
var args = new SocketConnectEventArgs(socket);
EventSink.InvokeSocketConnect(args);
if (!args.AllowConnection)
{
rejected = true;
if (socket.RemoteEndPoint is IPEndPoint ipep)
{
var ip = ipep.Address.ToString();
NetState.TraceDisconnect("Rejected by socket event handler", ip);
}
}
if (rejected)
{
socket.Send(_socketRejected, SocketFlags.None);
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
else
{
var ns = new NetState(socket);
_connectedQueue.Enqueue(ns);
}
return;
}
catch
catch (OperationCanceledException)
{
// ignored
return;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CloseSocket(Socket socket)
{
try
{
socket.Shutdown(SocketShutdown.Both);
}
catch
{
// ignored
}
socket.Close();
}
private static void ProcessConnection(Socket socket)
{
var ipLimiter = IPLimiter.Enabled;
try
{
// Clear out any sockets that have been connecting for too long
while (_socketsConnecting.Count > 0)
{
var socketTime = _socketsConnecting.Min; // Earliest connected socket
if (Core.TickCount - socketTime.ConnectedAt <= MaximumSocketIdleDelay)
{
break;
}
var socketToCheck = socketTime.NetState;
if (socketToCheck.Running && !socketToCheck.Seeded)
{
socketToCheck.Disconnect(null);
}
_socketsConnecting.Remove(socketTime);
}
var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address;
if (NetState.Instances.Count >= MaxConnections)
{
var ticks = Core.TickCount;
if (ticks - _nextMaximumSocketsReachedMessage > 0)
{
if (socket.RemoteEndPoint is IPEndPoint ipep)
{
var ip = ipep.Address.ToString();
logger.Warning("{Address} Failed (Maximum connections reached)", ip);
}
_nextMaximumSocketsReachedMessage = ticks + ListenerErrorMessageDelay;
}
CloseSocket(socket);
return;
}
if (ipLimiter && !IPLimiter.Verify(remoteIP))
{
TraceDisconnect("Past IP limit threshold", remoteIP);
logger.Debug("{Address} Past IP limit threshold", remoteIP);
CloseSocket(socket);
return;
}
if (Firewall.IsBlocked(remoteIP))
{
TraceDisconnect("Firewalled", remoteIP);
logger.Debug("{Address} Firewalled", remoteIP);
CloseSocket(socket);
return;
}
var ns = new NetState(socket);
_socketsConnecting.Add((Core.TickCount, ns));
ConnectedQueue.Enqueue(ns);
}
catch
{
// ignored
}
}
private static void TraceDisconnect(string reason, IPAddress ip)
{
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();
}
catch
{
// ignored
}
}
}

View file

@ -5,6 +5,7 @@ using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@ -158,408 +159,128 @@ public static class Utility
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint IPv4ToAddress(IPAddress ipAddress)
public static void ApplyCidrMask(ref ulong high, ref ulong low, int prefixLength, bool isMax)
{
if (ipAddress.IsIPv4MappedToIPv6)
// This should never happen, a 0 CIDR is not valid
if (prefixLength == 0)
{
ipAddress = ipAddress.MapToIPv4();
high = low = ~0UL;
return;
}
else if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
if (prefixLength == 128)
{
return;
}
if (prefixLength == 64)
{
low = 0;
return;
}
if (prefixLength < 64)
{
int bitsToFlip = 64 - prefixLength;
ulong highMask = isMax ? ~0UL >> bitsToFlip : ~0UL << (bitsToFlip + 1);
high = isMax ? high | highMask : high & highMask;
low = isMax ? ~0UL : 0UL;
}
else
{
int bitsToFlip = 128 - prefixLength;
ulong lowMask = isMax ? ~0UL >> (64 - bitsToFlip) : ~0UL << bitsToFlip;
low = isMax ? low | lowMask : low & lowMask;
}
}
// Converts an IPAddress to a UInt128 in IPv6 format
public static UInt128 ToUInt128(this IPAddress ip)
{
if (ip.AddressFamily == AddressFamily.InterNetwork && !ip.IsIPv4MappedToIPv6)
{
Span<byte> integer = stackalloc byte[4];
return !ip.TryWriteBytes(integer, out _)
? (UInt128)0
: new UInt128(0, 0xFFFF00000000UL | BinaryPrimitives.ReadUInt32BigEndian(integer));
}
Span<byte> bytes = stackalloc byte[16];
if (!ip.TryWriteBytes(bytes, out _))
{
return 0;
}
Span<byte> integer = stackalloc byte[4];
ipAddress.TryWriteBytes(integer, out var bytesWritten);
return bytesWritten != 4 ? 0 : BinaryPrimitives.ReadUInt32BigEndian(integer);
ulong high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]);
ulong low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8));
return new UInt128(high, low);
}
public static bool IPMatchClassC(IPAddress ip1, IPAddress ip2)
// Converts a UInt128 in IPv6 format to an IPAddress
public static IPAddress ToIpAddress(this UInt128 value, bool mapToIpv6 = false)
{
var a = IPv4ToAddress(ip1);
var b = IPv4ToAddress(ip2);
return a == 0 || b == 0 ? ip1.Equals(ip2) : (a & 0xFFFFFF) == (b & 0xFFFFFF);
}
public static bool IPMatchCIDR(IPAddress cidrAddress, IPAddress address, int cidrLength)
{
if (cidrAddress.AddressFamily == AddressFamily.InterNetwork)
// IPv4 mapped IPv6 address
if (!mapToIpv6 && value >= 0xFFFF00000000UL && value <= 0xFFFFFFFFFFFFUL)
{
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
return false;
}
cidrLength += 96;
var newAddress = IPAddress.HostToNetworkOrder((int)value);
return new IPAddress(unchecked((uint)newAddress));
}
cidrAddress = cidrAddress.MapToIPv6();
address = address.MapToIPv6();
Span<byte> bytes = stackalloc byte[16]; // 128 bits for IPv6 address
((IBinaryInteger<UInt128>)value).WriteBigEndian(bytes);
cidrLength = Math.Clamp(cidrLength, 0, 128);
return new IPAddress(bytes);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static UInt128 CreateCidrAddress(ReadOnlySpan<byte> bytes, int prefixLength, bool isMax)
{
ulong high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]);
ulong low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8));
if (prefixLength < 128)
{
ApplyCidrMask(ref high, ref low, prefixLength, isMax);
}
return new UInt128(high, low);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteMappedIPv6To(this IPAddress ipAddress, Span<byte> destination)
{
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
ipAddress.TryWriteBytes(destination, out _);
return;
}
destination[..8].Clear(); // Local init is off
BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(8, 4), 0xFFFF);
ipAddress.TryWriteBytes(destination.Slice(12, 4), out _);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool MatchClassC(this IPAddress ip1, IPAddress ip2) => ip1.MatchCidr(24, ip2);
public static bool MatchCidr(this IPAddress cidrAddress, int prefixLength, IPAddress address)
{
Span<byte> cidrBytes = stackalloc byte[16];
cidrAddress.TryWriteBytes(cidrBytes, out var _);
cidrAddress.WriteMappedIPv6To(cidrBytes);
Span<byte> addrBytes = stackalloc byte[16];
address.TryWriteBytes(addrBytes, out var _);
var i = 0;
int offset;
if (cidrLength < 32)
if (cidrAddress.AddressFamily != AddressFamily.InterNetworkV6)
{
offset = cidrLength;
}
else
{
var index = Math.DivRem(cidrLength, 32, out offset);
while (index > 0)
{
if (
BinaryPrimitives.ReadInt32BigEndian(cidrBytes.Slice(i, 4)) !=
BinaryPrimitives.ReadInt32BigEndian(addrBytes.Slice(i, 4))
)
{
return false;
}
i += 4;
--index;
}
prefixLength += 96; // 32 -> 128
}
if (offset == 0)
{
return true;
}
var min = CreateCidrAddress(cidrBytes, prefixLength, false);
var max = CreateCidrAddress(cidrBytes, prefixLength, true);
var ip = address.ToUInt128();
var c = BinaryPrimitives.ReadInt32BigEndian(cidrBytes.Slice(i, 4));
var a = BinaryPrimitives.ReadInt32BigEndian(addrBytes.Slice(i, 4));
var mask = (1 << (32 - offset)) - 1;
var min = ~mask & c;
var max = c | mask;
return a >= min && a <= max;
}
public static bool IsValidIP(string val) => IPMatch(val, IPAddress.Any, out var valid) || valid;
public static bool IPMatch(string val, IPAddress ip) => IPMatch(val, ip, out _);
public static bool IPMatch(string val, IPAddress ip, out bool valid)
{
var family = ip.AddressFamily;
var useIPv6 = family == AddressFamily.InterNetworkV6 || val.ContainsOrdinal(':');
ip = useIPv6 ? ip.MapToIPv6() : ip.MapToIPv4();
Span<byte> ipBytes = stackalloc byte[useIPv6 ? 16 : 4];
ip.TryWriteBytes(ipBytes, out _);
return useIPv6 ? IPv6Match(val, ipBytes, out valid) : IPv4Match(val, ipBytes, out valid);
}
public static bool IPv4Match(ReadOnlySpan<char> val, ReadOnlySpan<byte> ip, out bool valid)
{
var match = true;
valid = true;
var end = val.Length;
var byteIndex = 0;
var section = 0;
var number = 0;
var isRange = false;
var intBase = 10;
var endOfSection = false;
var sectionStart = 0;
var num = ip[byteIndex++];
for (var i = 0; i < end; i++)
{
var chr = val[i];
if (section >= 4)
{
valid = false;
return false;
}
switch (chr)
{
default:
{
if (!Uri.IsHexDigit(chr))
{
valid = false;
return false;
}
number = number * intBase + Uri.FromHex(chr);
break;
}
case 'x':
case 'X':
{
if (i == sectionStart)
{
intBase = 16;
break;
}
valid = false;
return false;
}
case '-':
{
if (i == sectionStart || i + 1 == end || val[i + 1] == '.')
{
valid = false;
return false;
}
// Only allows a single range in a section
if (isRange)
{
valid = false;
return false;
}
isRange = true;
match = match && num >= number;
number = 0;
break;
}
case '*':
{
if (i != sectionStart || i + 1 < end && val[i + 1] != '.')
{
valid = false;
return false;
}
isRange = true;
number = 255;
break;
}
case '.':
{
endOfSection = true;
break;
}
}
if (endOfSection || i + 1 == end)
{
if (number is < 0 or > 255)
{
valid = false;
return false;
}
match = match && (isRange ? num <= number : number == num);
if (++section < 4)
{
num = ip[byteIndex++];
}
intBase = 10;
number = 0;
endOfSection = false;
sectionStart = i + 1;
isRange = false;
}
}
return match;
}
public static bool IPv6Match(ReadOnlySpan<char> val, ReadOnlySpan<byte> ip, out bool valid)
{
valid = true;
// Start must be two `::` or a number
if (val[0] == ':' && val[1] != ':')
{
valid = false;
return false;
}
var match = true;
var end = val.Length;
var byteIndex = 2;
var section = 0;
var number = 0;
var isRange = false;
var endOfSection = false;
var sectionStart = 0;
var hasCompressor = false;
var num = BinaryPrimitives.ReadUInt16BigEndian(ip[..2]);
for (int i = 0; i < end; i++)
{
if (section > 7)
{
valid = false;
return false;
}
var chr = val[i];
// We are starting a new sequence, check the previous one then continue
switch (chr)
{
default:
{
if (!Uri.IsHexDigit(chr))
{
valid = false;
return false;
}
number = number * 16 + Uri.FromHex(chr);
break;
}
case '?':
{
logger.Debug("IP Match '?' character is not supported.");
valid = false;
return false;
}
// Range
case '-':
{
if (i == sectionStart || i + 1 == end || val[i + 1] == ':')
{
valid = false;
return false;
}
// Only allows a single range in a section
if (isRange)
{
valid = false;
return false;
}
isRange = true;
// Check low part of the range
match = match && num >= number;
number = 0;
break;
}
// Wild section
case '*':
{
if (i != sectionStart || i + 1 < end && val[i + 1] != ':')
{
valid = false;
return false;
}
isRange = true;
number = 65535;
break;
}
case ':':
{
endOfSection = true;
break;
}
}
if (!endOfSection && i + 1 != end)
{
continue;
}
if (++i == end || val[i] != ':' || section > 0)
{
match = match && (isRange ? num <= number : number == num);
// IPv4 matching at the end
if (section == 6 && num == 0xFFFF)
{
var ipv4 = val[(i + 1)..];
if (ipv4.Contains('.'))
{
return IPv4Match(ipv4, ip[^4..], out valid);
}
}
if (i == end)
{
break;
}
num = BinaryPrimitives.ReadUInt16BigEndian(ip.Slice(byteIndex, 2));
byteIndex += 2;
++section;
}
if (i < end && val[i] == ':')
{
if (hasCompressor)
{
valid = false;
return false;
}
int newSection;
if (i + 1 < end)
{
var remainingColons = val[(i + 1)..].Count(':');
// double colon must be at least 2 sections
// we need at least 1 section remaining out of 8
// This means 8 - 2 would be 6 sections (5 colons)
newSection = section + 2 + (5 - remainingColons);
if (newSection > 7)
{
valid = false;
return false;
}
}
else
{
newSection = 7;
}
var zeroEnd = (newSection + 1) * 2;
do
{
if (match)
{
if (num != 0)
{
match = false;
}
num = BinaryPrimitives.ReadUInt16BigEndian(ip.Slice(byteIndex, 2));
}
byteIndex += 2;
} while (byteIndex < zeroEnd);
section = newSection;
hasCompressor = true;
}
else
{
i--;
}
number = 0;
endOfSection = false;
sectionStart = i + 1;
isRange = false;
}
return match;
return ip >= min && ip <= max;
}
public static string FixHtml(string str)

View file

@ -103,7 +103,7 @@ public static class World
Span<byte> buffer = stackalloc byte[length].InitializePacket();
foreach (var ns in TcpServer.Instances)
foreach (var ns in NetState.Instances)
{
if (ns.Mobile == null)
{
@ -131,7 +131,7 @@ public static class World
Span<byte> buffer = stackalloc byte[length].InitializePacket();
foreach (var ns in TcpServer.Instances)
foreach (var ns in NetState.Instances)
{
if (ns.Mobile == null || ns.Mobile.AccessLevel < AccessLevel.GameMaster)
{