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:
parent
5f3de6537b
commit
4cd668ef61
38 changed files with 1117 additions and 1030 deletions
71
Projects/Server/Network/Firewall/BaseFirewallEntry.cs
Normal file
71
Projects/Server/Network/Firewall/BaseFirewallEntry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
75
Projects/Server/Network/Firewall/CidrFirewallEntry.cs
Normal file
75
Projects/Server/Network/Firewall/CidrFirewallEntry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
160
Projects/Server/Network/Firewall/Firewall.cs
Normal file
160
Projects/Server/Network/Firewall/Firewall.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
59
Projects/Server/Network/Firewall/IFirewallEntry.cs
Normal file
59
Projects/Server/Network/Firewall/IFirewallEntry.cs
Normal 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);
|
||||
}
|
||||
30
Projects/Server/Network/Firewall/SingleIpFirewallEntry.cs
Normal file
30
Projects/Server/Network/Firewall/SingleIpFirewallEntry.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2024 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* 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 *
|
||||
* 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 class SingleIpFirewallEntry : BaseFirewallEntry
|
||||
{
|
||||
public override UInt128 MinIpAddress { get; }
|
||||
|
||||
public override UInt128 MaxIpAddress => MinIpAddress;
|
||||
|
||||
public SingleIpFirewallEntry(string ipAddress) => MinIpAddress = IPAddress.Parse(ipAddress).ToUInt128();
|
||||
|
||||
public SingleIpFirewallEntry(IPAddress ipAddress) => MinIpAddress = ipAddress.ToUInt128();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue