From 4cd668ef619946509e43545ee54ec3abe68621d5 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:25:12 -0800 Subject: [PATCH] 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. --- .../Network/Firewall/FirewallEntryTests.cs | 35 ++ .../Tests/Utility/IPAddressTests.cs | 53 +-- Projects/Server/Main.cs | 1 - .../Network/Firewall/BaseFirewallEntry.cs | 71 +++ .../Network/Firewall/CidrFirewallEntry.cs | 75 +++ Projects/Server/Network/Firewall/Firewall.cs | 160 +++++++ .../Server/Network/Firewall/IFirewallEntry.cs | 59 +++ .../Firewall/SingleIpFirewallEntry.cs} | 29 +- Projects/Server/Network/IPLimiter.cs | 115 +++++ .../Server/Network/NetState/DumpNetStates.cs | 2 +- Projects/Server/Network/NetState/NetState.cs | 35 +- Projects/Server/Network/PingServer.cs | 61 ++- Projects/Server/Network/TcpServer.cs | 274 +++++++---- Projects/Server/Utilities/Utility.cs | 437 ++++-------------- Projects/Server/World/World.cs | 4 +- .../Tests/Firewall/FirewallEntryTests.cs | 20 - .../Accounting/AccessRestrictions.cs | 48 -- .../UOContent/Accounting/AccountHandler.cs | 27 +- Projects/UOContent/Accounting/Firewall.cs | 233 ---------- Projects/UOContent/Accounting/IPLimiter.cs | 62 --- .../Commands/Generic/Commands/Commands.cs | 2 +- .../IPAddressCommandImplementor.cs | 2 +- .../Implementors/OnlineCommandImplementor.cs | 2 +- Projects/UOContent/Commands/Handlers.cs | 4 +- Projects/UOContent/Engines/Help/PageQueue.cs | 2 +- Projects/UOContent/Gumps/AdminGump.cs | 67 +-- Projects/UOContent/Gumps/WhoGump.cs | 2 +- .../Special/House Raffle/HouseRaffleStone.cs | 2 +- Projects/UOContent/Misc/AdminFirewall.cs | 133 ++++++ Projects/UOContent/Misc/CrashGuard.cs | 4 +- Projects/UOContent/Misc/FoodDecay.cs | 2 +- Projects/UOContent/Misc/LightCycle.cs | 4 +- Projects/UOContent/Misc/LoginStats.cs | 2 +- Projects/UOContent/Misc/ServerList.cs | 44 +- Projects/UOContent/Misc/ShardPoller.cs | 2 +- Projects/UOContent/Misc/Weather.cs | 2 +- Projects/UOContent/Network/UOGateway.cs | 4 +- version.json | 2 +- 38 files changed, 1085 insertions(+), 998 deletions(-) create mode 100644 Projects/Server.Tests/Tests/Network/Firewall/FirewallEntryTests.cs create mode 100644 Projects/Server/Network/Firewall/BaseFirewallEntry.cs create mode 100644 Projects/Server/Network/Firewall/CidrFirewallEntry.cs create mode 100644 Projects/Server/Network/Firewall/Firewall.cs create mode 100644 Projects/Server/Network/Firewall/IFirewallEntry.cs rename Projects/Server/{Events/SocketConnectionEvent.cs => Network/Firewall/SingleIpFirewallEntry.cs} (57%) create mode 100644 Projects/Server/Network/IPLimiter.cs delete mode 100644 Projects/UOContent.Tests/Tests/Firewall/FirewallEntryTests.cs delete mode 100644 Projects/UOContent/Accounting/AccessRestrictions.cs delete mode 100644 Projects/UOContent/Accounting/Firewall.cs delete mode 100644 Projects/UOContent/Accounting/IPLimiter.cs create mode 100644 Projects/UOContent/Misc/AdminFirewall.cs diff --git a/Projects/Server.Tests/Tests/Network/Firewall/FirewallEntryTests.cs b/Projects/Server.Tests/Tests/Network/Firewall/FirewallEntryTests.cs new file mode 100644 index 0000000000..d60e90ea0e --- /dev/null +++ b/Projects/Server.Tests/Tests/Network/Firewall/FirewallEntryTests.cs @@ -0,0 +1,35 @@ +using System.Net; +using Server.Network; +using Xunit; + +namespace Server.Tests; + +public class FirewallEntryTests +{ + [Theory] + [InlineData("192.168.1.1", "192.168.1.1")] + [InlineData("::ffff:192.168.1.1", "192.168.1.1")] + [InlineData("ae45:c5c7:9372:2d3a:413c:6490:017d:2c18", "ae45:c5c7:9372:2d3a:413c:6490:017d:2c18")] + public void TestSingleIpFirewallEntry(string ip, string startAndEndIp) + { + var entry = new SingleIpFirewallEntry(ip); + + Assert.Equal(entry.MaxIpAddress, entry.MinIpAddress); + Assert.Equal(IPAddress.Parse(startAndEndIp), entry.MinIpAddress.ToIpAddress()); + } + + [Theory] + [InlineData("192.168.1.1/24", "192.168.1.0", "192.168.1.255")] + [InlineData("::ffff:10.25.3.250/112", "10.25.0.0", "10.25.255.255")] + [InlineData("::ffff:10.25.5.250/124", "10.25.5.240", "10.25.5.255")] + [InlineData("::ffff:192.168.1.1/120", "192.168.1.0", "192.168.1.255")] + [InlineData("d15e:d490:03cd:f9e1:95d8:8413:e6b8:e226/88", "D15E:D490:03CD:F9E1:95D8:8400::", "D15E:D490:03CD:F9E1:95D8:84FF:FFFF:FFFF")] + [InlineData("2001:4860:4860::8888/32", "2001:4860:0000:0000:0000:0000:0000:0000", "2001:4860:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF")] + public void TestCidrPatternIpFirewallEntry(string cidr, string startIp, string endIp) + { + var entry = new CidrFirewallEntry(cidr); + + Assert.Equal(IPAddress.Parse(startIp), entry.MinIpAddress.ToIpAddress()); + Assert.Equal(IPAddress.Parse(endIp), entry.MaxIpAddress.ToIpAddress()); + } +} diff --git a/Projects/Server.Tests/Tests/Utility/IPAddressTests.cs b/Projects/Server.Tests/Tests/Utility/IPAddressTests.cs index 5fa424b71d..8e7501e958 100644 --- a/Projects/Server.Tests/Tests/Utility/IPAddressTests.cs +++ b/Projects/Server.Tests/Tests/Utility/IPAddressTests.cs @@ -29,50 +29,25 @@ public void TestIPvCIDR(string cidr, string addr, int cidrLength, bool shouldMat var cidrAddress = IPAddress.Parse(cidr); var address = IPAddress.Parse(addr); - Assert.Equal(shouldMatch, Utility.IPMatchCIDR(cidrAddress, address, cidrLength)); + Assert.Equal(shouldMatch, cidrAddress.MatchCidr(cidrLength, address)); } [Theory] - [InlineData("192.168.1.*", "192.168.1.1", true, true)] - [InlineData("192.168.1.100", "192.168.1.1", false, true)] - [InlineData("192.168.*.100", "192.168.1.100", true, true)] - [InlineData("192.168.20-60.100", "192.168.37.100", true, true)] - [InlineData("192.168.20-60.100", "192.168.85.100", false, true)] - [InlineData("192.168.-.100", "192.168.85.100", false, false)] - [InlineData("192.168.x-.100", "192.168.85.100", false, false)] - [InlineData("192.168.x*.100", "192.168.85.100", false, false)] - [InlineData("192.168.**.100", "192.168.85.100", false, false)] - [InlineData("::1234:*", "::1234:5678", true, true)] - [InlineData("::1234-1238:1000", "::1236:1000", true, true)] - [InlineData("::1234-1238:1000", "::1239:1000", false, true)] - [InlineData("::10:*:1234-1238:1000", "::10:55A1:1235:1000", true, true)] - [InlineData("1024:*:1234::", "1024:8A13:1234::", true, true)] - [InlineData("::1024:*:1234::", "1024:8A13:1234::", false, false)] - [InlineData("::1024:*:1234:-", "::1024:8A13:1234", false, false)] - [InlineData("::1024:*1:1234", "::1024:8A13:1234", false, false)] - [InlineData("::1024:*-:1234", "::1024:8A13:1234", false, false)] - [InlineData("::1024:?1:1234", "::1024:8A13:1234", false, false)] - [InlineData("::1024:1_2:1234", "::1024:8A13:1234", false, false)] - [InlineData("172.16-31.*", "172.16.17.2", true, true)] - public void TestIPMatch(string val, string addr, bool shouldMatch, bool shouldBeValid) + [InlineData("::ffff:192.168.1.1", 0UL, 0xffffc0a80101UL)] + [InlineData("192.168.1.1", 0UL, 0xffffc0a80101UL)] + [InlineData("4cce:1490:4577:d72f:693e:b42e:3465:f3db", 0x4cce14904577d72fUL, 0x693eb42e3465f3db)] + public void TestIPAddressToUInt128(string ipString, ulong upper, ulong lower) { - var address = IPAddress.Parse(addr); - bool match = Utility.IPMatch(val, address, out var valid); - - Assert.Equal(shouldMatch, match); - Assert.Equal(shouldBeValid, valid); - } - - [Fact] - public void TestMixedIPv4Address() - { - var ip = IPAddress.Parse("::ffff:192.168.1.1"); - var expected = IPAddress.Parse("192.168.1.1"); - - Span integer = stackalloc byte[4]; - expected.TryWriteBytes(integer, out _); + var ip = IPAddress.Parse(ipString); + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } - Assert.Equal(BinaryPrimitives.ReadUInt32BigEndian(integer), Utility.IPv4ToAddress(ip)); + var actual = ip.ToUInt128(); + Assert.Equal(new UInt128(upper, lower), actual); + var actual2 = actual.ToIpAddress(); + Assert.Equal(ip, actual2); } } } diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index b414295940..d17c451085 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -564,7 +564,6 @@ public static void RunEventLoop() Timer.Slice(_tickCount); // Handle networking - TcpServer.Slice(); NetState.Slice(); PingServer.Slice(); diff --git a/Projects/Server/Network/Firewall/BaseFirewallEntry.cs b/Projects/Server/Network/Firewall/BaseFirewallEntry.cs new file mode 100644 index 0000000000..9834555ad9 --- /dev/null +++ b/Projects/Server/Network/Firewall/BaseFirewallEntry.cs @@ -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 . * + *************************************************************************/ + +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 destination, + out int charsWritten, + ReadOnlySpan 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; + } +} diff --git a/Projects/Server/Network/Firewall/CidrFirewallEntry.cs b/Projects/Server/Network/Firewall/CidrFirewallEntry.cs new file mode 100644 index 0000000000..fc64673878 --- /dev/null +++ b/Projects/Server/Network/Firewall/CidrFirewallEntry.cs @@ -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 . * + *************************************************************************/ + +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 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 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; + } +} diff --git a/Projects/Server/Network/Firewall/Firewall.cs b/Projects/Server/Network/Firewall/Firewall.cs new file mode 100644 index 0000000000..5a261488ba --- /dev/null +++ b/Projects/Server/Network/Firewall/Firewall.cs @@ -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 . * + *************************************************************************/ + +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 _isBlockedCache = new(); + + private static readonly ConcurrentQueue<(IFirewallEntry FirewallyEntry, bool Remove)> _firewallQueue = new(); + private static readonly SortedSet _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; + } +} diff --git a/Projects/Server/Network/Firewall/IFirewallEntry.cs b/Projects/Server/Network/Firewall/IFirewallEntry.cs new file mode 100644 index 0000000000..4a68f2441b --- /dev/null +++ b/Projects/Server/Network/Firewall/IFirewallEntry.cs @@ -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 . * + *************************************************************************/ + +using System; +using System.Net; + +namespace Server.Network; + +public interface IFirewallEntry : IComparable +{ + UInt128 MinIpAddress { get; } + UInt128 MaxIpAddress { get; } + + int IComparable.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); +} diff --git a/Projects/Server/Events/SocketConnectionEvent.cs b/Projects/Server/Network/Firewall/SingleIpFirewallEntry.cs similarity index 57% rename from Projects/Server/Events/SocketConnectionEvent.cs rename to Projects/Server/Network/Firewall/SingleIpFirewallEntry.cs index 9ac96dcd38..8b040b5fd6 100644 --- a/Projects/Server/Events/SocketConnectionEvent.cs +++ b/Projects/Server/Network/Firewall/SingleIpFirewallEntry.cs @@ -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 SocketConnect; + public SingleIpFirewallEntry(string ipAddress) => MinIpAddress = IPAddress.Parse(ipAddress).ToUInt128(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvokeSocketConnect(SocketConnectEventArgs e) => SocketConnect?.Invoke(e); + public SingleIpFirewallEntry(IPAddress ipAddress) => MinIpAddress = ipAddress.ToUInt128(); } diff --git a/Projects/Server/Network/IPLimiter.cs b/Projects/Server/Network/IPLimiter.cs new file mode 100644 index 0000000000..c0f1aec105 --- /dev/null +++ b/Projects/Server/Network/IPLimiter.cs @@ -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 . * + *************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.InteropServices; + +namespace Server.Misc; + +public static class IPLimiter +{ + private static readonly Dictionary _connectionAttempts = new(128); + private static readonly HashSet _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); + } +} diff --git a/Projects/Server/Network/NetState/DumpNetStates.cs b/Projects/Server/Network/NetState/DumpNetStates.cs index 6eb8d31971..1a7b4705e9 100644 --- a/Projects/Server/Network/NetState/DumpNetStates.cs +++ b/Projects/Server/Network/NetState/DumpNetStates.cs @@ -30,7 +30,7 @@ public static void DumpNetStatesCommand(CommandEventArgs args) 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}"); } diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 4c20680324..a52fbfc079 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -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, IValueLinkListNode _flushPending = new(2048); private static readonly Queue _flushedPartials = new(256); - private static readonly Queue _disposed = new(256); + private static readonly ConcurrentQueue _disposed = new(); private static readonly Queue _throttled = new(256); private static readonly Queue _throttledPending = new(256); public static NetStateCreatedCallback CreatedCallback { get; set; } + private static readonly HashSet _instances = new(2048); + public static IReadOnlySet Instances => _instances; + private readonly string _toString; private ClientVersion _version; private long _nextActivityCheck; @@ -152,8 +156,6 @@ public NetState(Socket connection) TraceException(ex); Disconnect("Unable to add socket to poll group"); } - - CreatedCallback?.Invoke(this); } // Sectors @@ -965,6 +967,16 @@ public static void FlushAll() 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 static void Slice() _throttled.Enqueue(_throttledPending.Dequeue()); } - int count = _pollGroup.Poll(_polledStates); + count = _pollGroup.Poll(_polledStates); if (count > 0) { @@ -1037,7 +1049,7 @@ public static void CheckAllAlive() { long curTicks = Core.TickCount; - foreach (var ns in TcpServer.Instances) + foreach (var ns in Instances) { ns.CheckAlive(curTicks); } @@ -1116,7 +1128,7 @@ public void Disconnect(string reason) public static void TraceDisconnect(string reason, string ip) { - if (reason == string.Empty) + if (string.IsNullOrWhiteSpace(reason)) { return; } @@ -1140,6 +1152,12 @@ public static void TraceDisconnect(string reason, string ip) 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 @@ private void Dispose() m.NetState = null; } - TcpServer.Instances.Remove(this); + _instances.Remove(this); + try { _pollGroup.Remove(Connection, _handle); @@ -1191,7 +1210,7 @@ private void Dispose() CityInfo = null; Connection = null; - var count = TcpServer.Instances.Count; + var count = Instances.Count; LogInfo(a != null ? $"Disconnected. [{count} Online] [{a}]" : $"Disconnected. [{count} Online]"); } diff --git a/Projects/Server/Network/PingServer.cs b/Projects/Server/Network/PingServer.cs index bfe36bb09e..3142f81f43 100644 --- a/Projects/Server/Network/PingServer.cs +++ b/Projects/Server/Network/PingServer.cs @@ -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 . * + *************************************************************************/ + 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 long _listenerErrorMessageDelay = 10000; // 10 seconds - private static long _nextMaximumSocketsReachedMessage; + private const int MaxConnectionsPerLoop = 128; - public static int MaxConnections { get; set; } + public static int MaxQueued { 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 void Configure() { 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 void Start() 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 void Start() } listeners.Add(listener); - BeginAcceptingSockets(listener); + Task.Run(() => BeginAcceptingUdpRequest(listener), cancellationToken).ConfigureAwait(false); } foreach (var ipep in listeningAddresses) @@ -126,36 +140,21 @@ public static UdpClient CreateListener(IPEndPoint ipep) 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 @@ private static async void BeginAcceptingSockets(UdpClient listener) } } - private static async void SendResponse(UdpClient listener, byte[] data, IPEndPoint ipep) + private static async Task SendResponse(UdpClient listener, byte[] data, IPEndPoint ipep) { try { diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 40a4eb5a90..502fd63f3d 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -13,13 +13,19 @@ * along with this program. If not, see . * *************************************************************************/ +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 - // 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; + private static readonly SemaphoreSlim _queueSemaphore = new(0); + private static readonly ConcurrentQueue _connectingQueue = []; + private static Thread _processConnectionsThread; - // AccountLoginReject BadComm - private static readonly byte[] _socketRejected = { 0x82, 0xFF }; + // Sanity. 256 * 1024 * 4096 = ~1.3GB of ram + public static int MaxConnections { get; set; } public static IPEndPoint[] ListeningAddresses { get; private set; } public static Socket[] Listeners { get; private set; } - public static HashSet Instances { get; } = new(2048); - private static readonly ConcurrentQueue _connectedQueue = new(); + // By default should sort T1 then T2 + public static readonly SortedSet<(long ConnectedAt, NetState NetState)> _socketsConnecting = []; + + public static ConcurrentQueue ConnectedQueue { get; } = []; public static void Configure() { @@ -51,45 +59,8 @@ public static void Configure() public static void Start() { - HashSet listeningAddresses = new HashSet(); - List listeners = new List(); - - 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 GetListeningAddresses(IPEndPoint ipep) => @@ -114,7 +85,7 @@ public static Socket CreateListener(IPEndPoint ipep) try { listener.Bind(ipep); - listener.Listen(32); + listener.Listen(256); return listener; } catch (SocketException se) @@ -138,74 +109,201 @@ public static Socket CreateListener(IPEndPoint ipep) 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 + { + var socket = await listener.AcceptAsync(cancellationToken); + _connectingQueue.Enqueue(socket); + _queueSemaphore.Release(); + } + catch(OperationCanceledException) { - Instances.Add(ns); - ns.LogInfo($"Connected. [{Instances.Count} Online]"); + 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 listeningAddresses = []; + List 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); + return; + } + catch (OperationCanceledException) + { + return; + } + } + } - if (!args.AllowConnection) + [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) { - rejected = true; - if (socket.RemoteEndPoint is IPEndPoint ipep) - { - var ip = ipep.Address.ToString(); - NetState.TraceDisconnect("Rejected by socket event handler", ip); - } + break; } - if (rejected) + var socketToCheck = socketTime.NetState; + if (socketToCheck.Running && !socketToCheck.Seeded) { - socket.Send(_socketRejected, SocketFlags.None); - socket.Shutdown(SocketShutdown.Both); - socket.Close(); + socketToCheck.Disconnect(null); } - else + + _socketsConnecting.Remove(socketTime); + } + + var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address; + + if (NetState.Instances.Count >= MaxConnections) + { + var ticks = Core.TickCount; + + if (ticks - _nextMaximumSocketsReachedMessage > 0) { - var ns = new NetState(socket); - _connectedQueue.Enqueue(ns); + 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; } - catch + + if (Firewall.IsBlocked(remoteIP)) { - // ignored + 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 } } } diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 1296a44abf..37124526c5 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -5,6 +5,7 @@ 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 void Intern(ref IPAddress ipAddress) } [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 0; + return; } - Span integer = stackalloc byte[4]; - ipAddress.TryWriteBytes(integer, out var bytesWritten); - return bytesWritten != 4 ? 0 : BinaryPrimitives.ReadUInt32BigEndian(integer); - } - - public static bool IPMatchClassC(IPAddress ip1, IPAddress ip2) - { - 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) + if (prefixLength == 64) { - if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - return false; - } - - cidrLength += 96; + low = 0; + return; } - cidrAddress = cidrAddress.MapToIPv6(); - address = address.MapToIPv6(); - - cidrLength = Math.Clamp(cidrLength, 0, 128); - - Span cidrBytes = stackalloc byte[16]; - cidrAddress.TryWriteBytes(cidrBytes, out var _); - - Span addrBytes = stackalloc byte[16]; - address.TryWriteBytes(addrBytes, out var _); - - var i = 0; - int offset; - - if (cidrLength < 32) + if (prefixLength < 64) { - offset = cidrLength; + int bitsToFlip = 64 - prefixLength; + ulong highMask = isMax ? ~0UL >> bitsToFlip : ~0UL << (bitsToFlip + 1); + + high = isMax ? high | highMask : high & highMask; + low = isMax ? ~0UL : 0UL; } 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; - } + int bitsToFlip = 128 - prefixLength; + ulong lowMask = isMax ? ~0UL >> (64 - bitsToFlip) : ~0UL << bitsToFlip; - i += 4; - --index; - } + low = isMax ? low | lowMask : low & lowMask; } + } - if (offset == 0) + // Converts an IPAddress to a UInt128 in IPv6 format + public static UInt128 ToUInt128(this IPAddress ip) + { + if (ip.AddressFamily == AddressFamily.InterNetwork && !ip.IsIPv4MappedToIPv6) { - return true; + Span integer = stackalloc byte[4]; + return !ip.TryWriteBytes(integer, out _) + ? (UInt128)0 + : new UInt128(0, 0xFFFF00000000UL | BinaryPrimitives.ReadUInt32BigEndian(integer)); } - var c = BinaryPrimitives.ReadInt32BigEndian(cidrBytes.Slice(i, 4)); - var a = BinaryPrimitives.ReadInt32BigEndian(addrBytes.Slice(i, 4)); + Span bytes = stackalloc byte[16]; + if (!ip.TryWriteBytes(bytes, out _)) + { + return 0; + } - var mask = (1 << (32 - offset)) - 1; - var min = ~mask & c; - var max = c | mask; + ulong high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]); + ulong low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8)); - return a >= min && a <= max; + return new UInt128(high, low); } - 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) + // Converts a UInt128 in IPv6 format to an IPAddress + public static IPAddress ToIpAddress(this UInt128 value, bool mapToIpv6 = false) { - var family = ip.AddressFamily; - var useIPv6 = family == AddressFamily.InterNetworkV6 || val.ContainsOrdinal(':'); - - ip = useIPv6 ? ip.MapToIPv6() : ip.MapToIPv4(); + // IPv4 mapped IPv6 address + if (!mapToIpv6 && value >= 0xFFFF00000000UL && value <= 0xFFFFFFFFFFFFUL) + { + var newAddress = IPAddress.HostToNetworkOrder((int)value); + return new IPAddress(unchecked((uint)newAddress)); + } - Span ipBytes = stackalloc byte[useIPv6 ? 16 : 4]; - ip.TryWriteBytes(ipBytes, out _); + Span bytes = stackalloc byte[16]; // 128 bits for IPv6 address + ((IBinaryInteger)value).WriteBigEndian(bytes); - return useIPv6 ? IPv6Match(val, ipBytes, out valid) : IPv4Match(val, ipBytes, out valid); + return new IPAddress(bytes); } - public static bool IPv4Match(ReadOnlySpan val, ReadOnlySpan ip, out bool valid) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 CreateCidrAddress(ReadOnlySpan bytes, int prefixLength, bool isMax) { - 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; + ulong high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]); + ulong low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8)); - var num = ip[byteIndex++]; - - for (var i = 0; i < end; i++) + if (prefixLength < 128) { - 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; - } + ApplyCidrMask(ref high, ref low, prefixLength, isMax); } - return match; + return new UInt128(high, low); } - public static bool IPv6Match(ReadOnlySpan val, ReadOnlySpan ip, out bool valid) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteMappedIPv6To(this IPAddress ipAddress, Span destination) { - valid = true; - - // Start must be two `::` or a number - if (val[0] == ':' && val[1] != ':') + if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) { - valid = false; - return false; + ipAddress.TryWriteBytes(destination, out _); + return; } - 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)); - } + destination[..8].Clear(); // Local init is off + BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(8, 4), 0xFFFF); + ipAddress.TryWriteBytes(destination.Slice(12, 4), out _); + } - byteIndex += 2; - } while (byteIndex < zeroEnd); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool MatchClassC(this IPAddress ip1, IPAddress ip2) => ip1.MatchCidr(24, ip2); - section = newSection; - hasCompressor = true; - } - else - { - i--; - } + public static bool MatchCidr(this IPAddress cidrAddress, int prefixLength, IPAddress address) + { + Span cidrBytes = stackalloc byte[16]; + cidrAddress.WriteMappedIPv6To(cidrBytes); - number = 0; - endOfSection = false; - sectionStart = i + 1; - isRange = false; + if (cidrAddress.AddressFamily != AddressFamily.InterNetworkV6) + { + prefixLength += 96; // 32 -> 128 } - return match; + var min = CreateCidrAddress(cidrBytes, prefixLength, false); + var max = CreateCidrAddress(cidrBytes, prefixLength, true); + var ip = address.ToUInt128(); + + return ip >= min && ip <= max; } public static string FixHtml(string str) diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index ed2a37ed1d..2344baad4b 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -103,7 +103,7 @@ public static void Broadcast(int hue, bool ascii, string text) Span 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 void BroadcastStaff(int hue, bool ascii, string text) Span 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) { diff --git a/Projects/UOContent.Tests/Tests/Firewall/FirewallEntryTests.cs b/Projects/UOContent.Tests/Tests/Firewall/FirewallEntryTests.cs deleted file mode 100644 index 79c1fd0dd8..0000000000 --- a/Projects/UOContent.Tests/Tests/Firewall/FirewallEntryTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Net; -using Xunit; - -namespace Server.Tests -{ - public class FirewallEntryTests - { - [Theory] - [InlineData("192.168.1.1/24", typeof(Firewall.CIDRFirewallEntry), "192.168.1.50")] - [InlineData("192.168.*.100-200", typeof(Firewall.WildcardIPFirewallEntry), "192.168.30.150")] - [InlineData("::1234:*:1000-1234", typeof(Firewall.WildcardIPFirewallEntry), "::1234:5678:1150")] - public void TestFirewallEntry(string entry, Type entryType, string ipToMatch) - { - var firewallEntry = Firewall.ToFirewallEntry(entry); - Assert.IsType(entryType, firewallEntry); - Assert.True(firewallEntry.IsBlocked(IPAddress.Parse(ipToMatch))); - } - } -} diff --git a/Projects/UOContent/Accounting/AccessRestrictions.cs b/Projects/UOContent/Accounting/AccessRestrictions.cs deleted file mode 100644 index 4522e558ce..0000000000 --- a/Projects/UOContent/Accounting/AccessRestrictions.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.IO; -using System.Net; -using Server.Logging; -using Server.Misc; - -namespace Server -{ - public static class AccessRestrictions - { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(AccessRestrictions)); - - public static void Initialize() - { - EventSink.SocketConnect += EventSink_SocketConnect; - } - - private static void EventSink_SocketConnect(SocketConnectEventArgs e) - { - try - { - var ip = (e.Connection.RemoteEndPoint as IPEndPoint)?.Address; - - if (Firewall.IsBlocked(ip)) - { - logger.Information("Client: {IP}: Firewall blocked connection attempt.", ip); - e.AllowConnection = false; - return; - } - - if (IPLimiter.SocketBlock && !IPLimiter.Verify(ip)) - { - logger.Warning("Client: {IP}: Past IP limit threshold", ip); - - using (var op = new StreamWriter("ipLimits.log", true)) - { - op.WriteLine("{0}\tPast IP limit threshold\t{1}", ip, Core.Now); - } - - e.AllowConnection = false; - } - } - catch - { - e.AllowConnection = false; - } - } - } -} diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index abd5c4286b..729fd67664 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -136,7 +136,7 @@ public static void Password_OnCommand(CommandEventArgs e) { var ipAddress = ns.Address; - if (Utility.IPMatchClassC(accessList[0], ipAddress)) + if (accessList[0].MatchClassC(ipAddress)) { acct.SetPassword(pass); from.SendMessage("The password to your account has changed."); @@ -302,19 +302,6 @@ private static Account CreateAccount(NetState state, string un, string pw) public static void EventSink_AccountLogin(AccountLoginEventArgs e) { - if (!IPLimiter.SocketBlock && !IPLimiter.Verify(e.State.Address)) - { - e.Accepted = false; - e.RejectReason = ALRReason.InUse; - - logger.Information("Login: {NetState}: Past IP limit threshold", e.State); - - using var op = new StreamWriter("ipLimits.log", true); - op.WriteLine($"{e.State}\tPast IP limit threshold\t{Core.Now}"); - - return; - } - var un = e.Username; var pw = e.Password; @@ -371,18 +358,6 @@ public static void EventSink_AccountLogin(AccountLoginEventArgs e) public static void EventSink_GameLogin(GameLoginEventArgs e) { - if (!IPLimiter.SocketBlock && !IPLimiter.Verify(e.State.Address)) - { - e.Accepted = false; - - logger.Warning("Login: {NetState} Past IP limit threshold", e.State); - - using var op = new StreamWriter("ipLimits.log", true); - op.WriteLine($"{e.State}\tPast IP limit threshold\t{Core.Now}"); - - return; - } - var un = e.Username; var pw = e.Password; diff --git a/Projects/UOContent/Accounting/Firewall.cs b/Projects/UOContent/Accounting/Firewall.cs deleted file mode 100644 index 88ea0471e0..0000000000 --- a/Projects/UOContent/Accounting/Firewall.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Runtime.CompilerServices; -using CommunityToolkit.HighPerformance; - -namespace Server -{ - public static class Firewall - { - private const string firewallConfigPath = "firewall.cfg"; - - static Firewall() - { - Set = new HashSet(); - - if (File.Exists(firewallConfigPath)) - { - using var ip = new StreamReader(firewallConfigPath); - string line; - - while ((line = ip.ReadLine()) != null) - { - line = line.Trim(); - - if (line.Length == 0) - { - continue; - } - - Set.Add(ToFirewallEntry(line)); - } - } - } - - public static HashSet Set { get; } - - public static IFirewallEntry ToFirewallEntry(object entry) - { - return entry switch - { - IFirewallEntry firewallEntry => firewallEntry, - IPAddress address => new IPFirewallEntry(address), - string s => ToFirewallEntry(s), - _ => null - }; - } - - public static IFirewallEntry ToFirewallEntry(string entry) - { - if (entry == null) - { - return null; - } - - if (IPAddress.TryParse(entry, out var addr)) - { - return new IPFirewallEntry(addr); - } - - // Try CIDR parse - var tokenizer = entry.Tokenize('/'); - var ip = tokenizer.MoveNext() ? tokenizer.Current : null; - var length = tokenizer.MoveNext() ? tokenizer.Current : null; - - if ( - length != null && - IPAddress.TryParse(ip, out var cidrPrefix) && - int.TryParse(length, out var cidrLength) - ) - { - return new CIDRFirewallEntry(cidrPrefix, cidrLength); - } - - return new WildcardIPFirewallEntry(entry); - } - - public static void Remove(object obj) - { - var entry = ToFirewallEntry(obj); - - if (entry != null) - { - Set.Remove(entry); - Save(); - } - } - - public static void Add(object obj) - { - Add(ToFirewallEntry(obj)); - } - - public static void Add(IFirewallEntry entry) - { - Set.Add(entry); - Save(); - } - - public static void Add(string pattern) - { - Add(ToFirewallEntry(pattern)); - } - - public static void Add(IPAddress ip) - { - Add(ToFirewallEntry(ip)); - } - - public static void Save() - { - using var op = new StreamWriter(firewallConfigPath); - foreach (var entry in Set) - { - op.WriteLine(entry); - } - } - - public static bool IsBlocked(IPAddress ip) - { - foreach (var entry in Set) - { - if (entry.IsBlocked(ip)) - { - return true; - } - } - - return false; - } - - public interface IFirewallEntry - { - bool IsBlocked(IPAddress address); - } - - public class IPFirewallEntry : IFirewallEntry - { - private readonly IPAddress m_Address; - - public IPFirewallEntry(IPAddress address) => m_Address = address; - - public bool IsBlocked(IPAddress address) => m_Address.Equals(address); - - public override string ToString() => m_Address.ToString(); - - public override bool Equals(object obj) - { - return obj switch - { - IPAddress => obj.Equals(m_Address), - string s when IPAddress.TryParse(s, out var otherAddress) => otherAddress.Equals(m_Address), - IPFirewallEntry entry => m_Address.Equals(entry.m_Address), - _ => false - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => m_Address.GetHashCode(); - } - - public class CIDRFirewallEntry : IFirewallEntry - { - private readonly int m_CIDRLength; - private readonly IPAddress m_CIDRPrefix; - - public CIDRFirewallEntry(IPAddress cidrPrefix, int cidrLength) - { - m_CIDRPrefix = cidrPrefix; - m_CIDRLength = cidrLength; - } - - public bool IsBlocked(IPAddress address) => Utility.IPMatchCIDR(m_CIDRPrefix, address, m_CIDRLength); - - public override string ToString() => $"{m_CIDRPrefix}/{m_CIDRLength}"; - - public override bool Equals(object obj) - { - if (obj is string entry) - { - var str = entry.Split('/'); - - if (str.Length == 2) - { - if (IPAddress.TryParse(str[0], out var cidrPrefix)) - { - if (int.TryParse(str[1], out var cidrLength)) - { - return m_CIDRPrefix.Equals(cidrPrefix) && m_CIDRLength.Equals(cidrLength); - } - } - } - } - else if (obj is CIDRFirewallEntry cidrEntry) - { - return m_CIDRPrefix.Equals(cidrEntry.m_CIDRPrefix) && m_CIDRLength.Equals(cidrEntry.m_CIDRLength); - } - - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => HashCode.Combine(m_CIDRPrefix, m_CIDRLength); - } - - public class WildcardIPFirewallEntry : IFirewallEntry - { - private readonly string m_Entry; - - private bool m_Valid = true; - - public WildcardIPFirewallEntry(string entry) => m_Entry = entry; - - public bool IsBlocked(IPAddress address) => m_Valid && Utility.IPMatch(m_Entry, address, out m_Valid); - - public override string ToString() => m_Entry; - - public override bool Equals(object obj) - { - if (obj is string) - { - return obj.Equals(m_Entry); - } - - return obj is WildcardIPFirewallEntry entry && m_Entry == entry.m_Entry; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => m_Entry.GetHashCode(StringComparison.Ordinal); - } - } -} diff --git a/Projects/UOContent/Accounting/IPLimiter.cs b/Projects/UOContent/Accounting/IPLimiter.cs deleted file mode 100644 index 49c72af892..0000000000 --- a/Projects/UOContent/Accounting/IPLimiter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Net; -using Server.Network; - -namespace Server.Misc -{ - public static class IPLimiter - { - public static readonly IPAddress[] Exemptions = - { - IPAddress.Parse( "127.0.0.1" ) - }; - - public static bool Enabled { get; private set; } - public static bool SocketBlock { get; private set; } - public static int MaxAddresses { get; private set; } - - public static void Configure() - { - Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true); - SocketBlock = ServerConfiguration.GetOrUpdateSetting("ipLimiter.blockAtConnection", true); - MaxAddresses = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 10); - } - - 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 count = 0; - - foreach (var ns in TcpServer.Instances) - { - if (ourAddress.Equals(ns.Address)) - { - ++count; - - if (count >= MaxAddresses) - { - return false; - } - } - } - - return true; - } - } -} diff --git a/Projects/UOContent/Commands/Generic/Commands/Commands.cs b/Projects/UOContent/Commands/Generic/Commands/Commands.cs index b780ae3904..e70533aa58 100644 --- a/Projects/UOContent/Commands/Generic/Commands/Commands.cs +++ b/Projects/UOContent/Commands/Generic/Commands/Commands.cs @@ -1137,7 +1137,7 @@ public override void Execute(CommandEventArgs e, object obj) try { - Firewall.Add(state.Address); + AdminFirewall.Add(state.Address); AddResponse("They have been firewalled."); } catch (Exception ex) diff --git a/Projects/UOContent/Commands/Generic/Implementors/IPAddressCommandImplementor.cs b/Projects/UOContent/Commands/Generic/Implementors/IPAddressCommandImplementor.cs index 39f06d8ab5..faf39175e9 100644 --- a/Projects/UOContent/Commands/Generic/Implementors/IPAddressCommandImplementor.cs +++ b/Projects/UOContent/Commands/Generic/Implementors/IPAddressCommandImplementor.cs @@ -38,7 +38,7 @@ public override void Compile(Mobile from, BaseCommand command, ref string[] args var list = new List(); var addresses = new List(); - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var mob = ns.Mobile; diff --git a/Projects/UOContent/Commands/Generic/Implementors/OnlineCommandImplementor.cs b/Projects/UOContent/Commands/Generic/Implementors/OnlineCommandImplementor.cs index 1120b6f0f0..b3144f6a88 100644 --- a/Projects/UOContent/Commands/Generic/Implementors/OnlineCommandImplementor.cs +++ b/Projects/UOContent/Commands/Generic/Implementors/OnlineCommandImplementor.cs @@ -36,7 +36,7 @@ public override void Compile(Mobile from, BaseCommand command, ref string[] args var list = new List(); - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var mob = ns.Mobile; diff --git a/Projects/UOContent/Commands/Handlers.cs b/Projects/UOContent/Commands/Handlers.cs index c629573290..53cd5152ee 100644 --- a/Projects/UOContent/Commands/Handlers.cs +++ b/Projects/UOContent/Commands/Handlers.cs @@ -753,7 +753,7 @@ public static void BroadcastMessage_OnCommand(CommandEventArgs e) public static void BroadcastMessage(AccessLevel ac, int hue, string message) { - foreach (var state in TcpServer.Instances) + foreach (var state in NetState.Instances) { var m = state.Mobile; @@ -849,7 +849,7 @@ public static void Light_OnCommand(CommandEventArgs e) [Description("View some stats about the server.")] public static void Stats_OnCommand(CommandEventArgs e) { - e.Mobile.SendMessage($"Open Connections: {TcpServer.Instances.Count}"); + e.Mobile.SendMessage($"Open Connections: {NetState.Instances.Count}"); e.Mobile.SendMessage($"Mobiles: {World.Mobiles.Count}"); e.Mobile.SendMessage($"Items: {World.Items.Count}"); } diff --git a/Projects/UOContent/Engines/Help/PageQueue.cs b/Projects/UOContent/Engines/Help/PageQueue.cs index c64290961f..4c2cc86681 100644 --- a/Projects/UOContent/Engines/Help/PageQueue.cs +++ b/Projects/UOContent/Engines/Help/PageQueue.cs @@ -231,7 +231,7 @@ public static void Enqueue(PageEntry entry) var isStaffOnline = false; - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var m = ns.Mobile; diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index 8fca4fff67..810dc22e83 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -171,10 +171,10 @@ public AdminGump( AddLabel(150, 150, LabelHue, banned.ToString()); AddLabel(20, 170, LabelHue, "Firewalled:"); - AddLabel(150, 170, LabelHue, Firewall.Set.Count.ToString()); + AddLabel(150, 170, LabelHue, AdminFirewall.Set.Count.ToString()); AddLabel(20, 190, LabelHue, "Clients:"); - AddLabel(150, 190, LabelHue, TcpServer.Instances.Count.ToString()); + AddLabel(150, 190, LabelHue, NetState.Instances.Count.ToString()); AddLabel(20, 210, LabelHue, "Mobiles:"); AddLabel(150, 210, LabelHue, World.Mobiles.Count.ToString()); @@ -437,7 +437,7 @@ public AdminGump( { if (m_List == null) { - var states = TcpServer.Instances.ToList(); + var states = NetState.Instances.ToList(); states.Sort(NetStateComparer.Instance); m_List = states.ToList(); @@ -1225,7 +1225,7 @@ public AdminGump( { AddFirewallHeader(); - m_List ??= Firewall.Set.ToList(); + m_List ??= AdminFirewall.Set.ToList(); AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address"); @@ -1275,7 +1275,7 @@ public AdminGump( { AddFirewallHeader(); - if (state is not Firewall.IFirewallEntry firewallEntry) + if (state is not IFirewallEntry firewallEntry) { break; } @@ -1804,7 +1804,7 @@ public static void FirewallShared_Callback(Mobile from, bool okay, Account a) { for (var i = 0; i < a.LoginIPs.Length; ++i) { - Firewall.Add(a.LoginIPs[i]); + AdminFirewall.Add(a.LoginIPs[i]); } notice = "All addresses in the list have been firewalled."; @@ -1828,7 +1828,7 @@ public static void Firewall_Callback(Mobile from, bool okay, Account a, object t if (okay) { - Firewall.Add(toFirewall); + AdminFirewall.Add(toFirewall); notice = $"{toFirewall} : Added to firewall."; } @@ -2427,7 +2427,7 @@ public override void OnResponse(NetState sender, RelayInfo info) { var count = 0; - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var a = ns.Account; @@ -2533,7 +2533,7 @@ public override void OnResponse(NetState sender, RelayInfo info) } else { - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { bool isMatch; @@ -3626,7 +3626,7 @@ public override void OnResponse(NetState sender, RelayInfo info) } else { - foreach (var check in Firewall.Set) + foreach (var check in AdminFirewall.Set) { var checkStr = check.ToString(); @@ -3692,42 +3692,47 @@ public override void OnResponse(NetState sender, RelayInfo info) m_PageType, m_ListPage, m_List, - "You must enter an address or pattern to add.", - m_State - ) - ); - } - else if (!Utility.IsValidIP(text)) - { - from.SendGump( - new AdminGump( - from, - m_PageType, - m_ListPage, - m_List, - "That is not a valid address or pattern.", + "You must enter an address or CIDR to add.", m_State ) ); } else { - object toAdd = Firewall.ToFirewallEntry(text); + IFirewallEntry firewallEntry; + try + { + firewallEntry = AdminFirewall.ToFirewallEntry(text); + } + catch + { + from.SendGump( + new AdminGump( + from, + m_PageType, + m_ListPage, + m_List, + "That is not a valid address or CIDR.", + m_State + ) + ); + break; + } CommandLogging.WriteLine( from, - $"{from.AccessLevel} {CommandLogging.Format(from)} firewalling {toAdd}" + $"{from.AccessLevel} {CommandLogging.Format(from)} firewalling {firewallEntry}" ); - Firewall.Add(toAdd); + AdminFirewall.Add(firewallEntry); from.SendGump( new AdminGump( from, AdminGumpPage.FirewallInfo, 0, null, - $"{toAdd} : Added to firewall.", - toAdd + $"{firewallEntry} : Added to firewall.", + firewallEntry ) ); } @@ -3751,14 +3756,14 @@ public override void OnResponse(NetState sender, RelayInfo info) } case 3: { - if (m_State is Firewall.IFirewallEntry) + if (m_State is IFirewallEntry) { CommandLogging.WriteLine( from, $"{from.AccessLevel} {CommandLogging.Format(from)} removing {m_State} from firewall list" ); - Firewall.Remove(m_State); + AdminFirewall.Remove(m_State); from.SendGump( new AdminGump( from, diff --git a/Projects/UOContent/Gumps/WhoGump.cs b/Projects/UOContent/Gumps/WhoGump.cs index 8f925474cb..0833afe3bd 100644 --- a/Projects/UOContent/Gumps/WhoGump.cs +++ b/Projects/UOContent/Gumps/WhoGump.cs @@ -60,7 +60,7 @@ public static List BuildList(Mobile owner, string rawFilter) var list = new List(); - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var m = ns.Mobile; diff --git a/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs b/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs index 9b6d38755a..14761bbcd3 100644 --- a/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs +++ b/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs @@ -289,7 +289,7 @@ private bool IsAtIPLimit(Mobile from) foreach (var entry in Entries) { - if (Utility.IPMatchClassC(entry.Address, address)) + if (entry.Address.MatchClassC(address)) { if (++tickets >= EntryLimitPerIP) { diff --git a/Projects/UOContent/Misc/AdminFirewall.cs b/Projects/UOContent/Misc/AdminFirewall.cs new file mode 100644 index 0000000000..4f0ab02ae3 --- /dev/null +++ b/Projects/UOContent/Misc/AdminFirewall.cs @@ -0,0 +1,133 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.CompilerServices; +using Server.Logging; +using Server.Network; + +namespace Server; + +public static class AdminFirewall +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(AdminFirewall)); + + private static readonly HashSet _firewallSet = []; + private const string firewallConfigPath = "firewall.cfg"; + + public static void Configure() + { + if (File.Exists(firewallConfigPath)) + { + var searchValues = SearchValues.Create("*Xx?"); + + using var ip = new StreamReader(firewallConfigPath); + + while (ip.ReadLine() is { } line) + { + line = line.Trim(); + + if (line.Length == 0) + { + continue; + } + + if (line.AsSpan().ContainsAny(searchValues)) + { + logger.Warning("Legacy firewall entry \"{Entry}\" ignored", line); + continue; + } + + Add(ToFirewallEntry(line), false); + } + } + } + + // Note: This is not optimized, so do not use this in hot paths + public static IReadOnlySet Set => _firewallSet; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IFirewallEntry ToFirewallEntry(object entry) + { + return entry switch + { + IFirewallEntry firewallEntry => firewallEntry, + IPAddress address => new SingleIpFirewallEntry(address), + string s => ToFirewallEntry(s), + _ => null + }; + } + + public static IFirewallEntry ToFirewallEntry(string entry) + { + if (entry == null) + { + return null; + } + + try + { + var rangeSeparator = entry.IndexOf('-'); + if (rangeSeparator > -1) + { + return new CidrFirewallEntry( + IPAddress.Parse(entry.AsSpan(0, rangeSeparator)), + IPAddress.Parse(entry.AsSpan(rangeSeparator + 1)) + ); + } + + // CIDR notation + if (entry.IndexOf('/') > -1) + { + return new CidrFirewallEntry(entry); + } + + return new SingleIpFirewallEntry(entry); + } + catch + { + return null; + } + } + + public static void Remove(object obj, bool save = true) + { + var entry = ToFirewallEntry(obj); + + if (entry != null) + { + _firewallSet.Remove(entry); + Firewall.RequestRemoveEntry(entry); // Request that the TcpServer also remove the entry + + if (save) + { + Save(); + } + } + } + + public static bool Add(object obj) => Add(ToFirewallEntry(obj)); + + public static bool Add(IFirewallEntry entry, bool save = true) + { + var added = _firewallSet.Add(entry); + Firewall.RequestAddEntry(entry); // Request that the TcpServer also add the entry + + if (save) + { + Save(); + } + + return added; + } + + public static void Save() + { + using var op = new StreamWriter(firewallConfigPath); + foreach (var entry in Set) + { + op.WriteLine(entry); + } + } +} diff --git a/Projects/UOContent/Misc/CrashGuard.cs b/Projects/UOContent/Misc/CrashGuard.cs index 592cc724b0..a775297b83 100644 --- a/Projects/UOContent/Misc/CrashGuard.cs +++ b/Projects/UOContent/Misc/CrashGuard.cs @@ -178,11 +178,11 @@ private static void GenerateCrashReport(ServerCrashedEventArgs e) try { - var states = TcpServer.Instances; + var states = NetState.Instances; op.WriteLine($"- Count: {states.Count}"); - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { op.Write($"+ {ns}:"); diff --git a/Projects/UOContent/Misc/FoodDecay.cs b/Projects/UOContent/Misc/FoodDecay.cs index 7174db5dd6..8cb1c21d2d 100644 --- a/Projects/UOContent/Misc/FoodDecay.cs +++ b/Projects/UOContent/Misc/FoodDecay.cs @@ -21,7 +21,7 @@ protected override void OnTick() public static void FoodDecay() { - foreach (var state in TcpServer.Instances) + foreach (var state in NetState.Instances) { HungerDecay(state.Mobile); ThirstDecay(state.Mobile); diff --git a/Projects/UOContent/Misc/LightCycle.cs b/Projects/UOContent/Misc/LightCycle.cs index 1b592d8e8e..301952f9f3 100644 --- a/Projects/UOContent/Misc/LightCycle.cs +++ b/Projects/UOContent/Misc/LightCycle.cs @@ -21,7 +21,7 @@ public static int LevelOverride { m_LevelOverride = value; - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var m = ns.Mobile; @@ -98,7 +98,7 @@ public LightCycleTimer() : base(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(5. protected override void OnTick() { - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { ns.Mobile?.CheckLightLevels(false); } diff --git a/Projects/UOContent/Misc/LoginStats.cs b/Projects/UOContent/Misc/LoginStats.cs index e47acbcdf6..0ea213cd01 100644 --- a/Projects/UOContent/Misc/LoginStats.cs +++ b/Projects/UOContent/Misc/LoginStats.cs @@ -12,7 +12,7 @@ public static void Initialize() private static void EventSink_Login(Mobile m) { - var userCount = TcpServer.Instances.Count; + var userCount = NetState.Instances.Count; var itemCount = World.Items.Count; var mobileCount = World.Mobiles.Count; diff --git a/Projects/UOContent/Misc/ServerList.cs b/Projects/UOContent/Misc/ServerList.cs index 1b60f14d99..e208f7c9b5 100644 --- a/Projects/UOContent/Misc/ServerList.cs +++ b/Projects/UOContent/Misc/ServerList.cs @@ -4,6 +4,7 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using Server.Logging; +using Server.Network; namespace Server.Misc { @@ -153,18 +154,39 @@ private static bool HasPublicIPAddress() return false; } - // 10.0.0.0/8 - // 172.16.0.0/12 - // 192.168.0.0/16 - // 169.254.0.0/16 - // 100.64.0.0/10 RFC 6598 private static bool IsPrivateNetwork(IPAddress ip) => - ip.AddressFamily != AddressFamily.InterNetworkV6 && - (Utility.IPMatch("192.168.*", ip) || - Utility.IPMatch("10.*", ip) || - Utility.IPMatch("172.16-31.*", ip) || - Utility.IPMatch("169.254.*", ip) || - Utility.IPMatch("100.64-127.*", ip)); + ip.AddressFamily switch + { + AddressFamily.InterNetwork => IsPrivateNetworkV4(ip), + AddressFamily.InterNetworkV6 => IsPrivateNetworkV6(ip), + _ => false + }; + + private static readonly IFirewallEntry[] _privateNetworkV4 = + [ + new CidrFirewallEntry("192.168.0.0/16"), + new CidrFirewallEntry("10.0.0.0/8"), + new CidrFirewallEntry("172.16.0.0/12"), + new CidrFirewallEntry("169.254.0.0/16"), + new CidrFirewallEntry("100.64.0.0/10") + ]; + + private static readonly IFirewallEntry[] _privateNetworkV6 = + [ + new CidrFirewallEntry("fc00::/7"), + new CidrFirewallEntry("fe80::/10") + ]; + + private static bool IsPrivateNetworkV4(IPAddress ip) => + _privateNetworkV4[0].IsBlocked(ip) || + _privateNetworkV4[1].IsBlocked(ip) || + _privateNetworkV4[2].IsBlocked(ip) || + _privateNetworkV4[3].IsBlocked(ip) || + _privateNetworkV4[4].IsBlocked(ip); + + private static bool IsPrivateNetworkV6(IPAddress ip) => + _privateNetworkV6[0].IsBlocked(ip) || + _privateNetworkV6[1].IsBlocked(ip); private const string _ipifyUrl = "https://api.ipify.org"; diff --git a/Projects/UOContent/Misc/ShardPoller.cs b/Projects/UOContent/Misc/ShardPoller.cs index 16d34c5ff0..f12e4a39cb 100644 --- a/Projects/UOContent/Misc/ShardPoller.cs +++ b/Projects/UOContent/Misc/ShardPoller.cs @@ -319,7 +319,7 @@ public bool HasAlreadyVoted(NetState ns) for (var i = 0; i < Voters.Length; ++i) { - if (Utility.IPMatchClassC(Voters[i], ipAddress)) + if (Voters[i].MatchClassC(ipAddress)) { return true; } diff --git a/Projects/UOContent/Misc/Weather.cs b/Projects/UOContent/Misc/Weather.cs index f02977ff2c..a29b7e9851 100644 --- a/Projects/UOContent/Misc/Weather.cs +++ b/Projects/UOContent/Misc/Weather.cs @@ -345,7 +345,7 @@ public virtual void OnTick() type = 2; } - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var mob = ns.Mobile; diff --git a/Projects/UOContent/Network/UOGateway.cs b/Projects/UOContent/Network/UOGateway.cs index 439b228d88..62970c0acf 100644 --- a/Projects/UOContent/Network/UOGateway.cs +++ b/Projects/UOContent/Network/UOGateway.cs @@ -37,7 +37,7 @@ public static void QueryCompactShardStats(NetState state, SpanReader reader) { state.SendCompactShardStats( (uint)(Core.Uptime / 1000), - TcpServer.Instances.Count - 1, // Shame if you modify this! + NetState.Instances.Count - 1, // Shame if you modify this! World.Items.Count, World.Mobiles.Count, GC.GetTotalMemory(false) @@ -50,7 +50,7 @@ public static void QueryExtendedShardStats(NetState state, SpanReader reader) state.SendExtendedShardStats( ServerList.ServerName, (int)(Core.Uptime / ticksInHour), - TcpServer.Instances.Count - 1, // Shame if you modify this! + NetState.Instances.Count - 1, // Shame if you modify this! World.Items.Count, World.Mobiles.Count, (int)(GC.GetTotalMemory(false) / 1024) diff --git a/version.json b/version.json index 4ed56ec739..1477d35bef 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.11.2" + "version": "0.12.1" }