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 000000000..d60e90ea0 --- /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 5fa424b71..8e7501e95 100644 --- a/Projects/Server.Tests/Tests/Utility/IPAddressTests.cs +++ b/Projects/Server.Tests/Tests/Utility/IPAddressTests.cs @@ -29,50 +29,25 @@ namespace Server.Tests 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); + var ip = IPAddress.Parse(ipString); + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } - 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 _); - - 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 b41429594..d17c45108 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -564,7 +564,6 @@ public static class Core 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 000000000..9834555ad --- /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 000000000..fc6467387 --- /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 000000000..5a261488b --- /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 000000000..4a68f2441 --- /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 9ac96dcd3..8b040b5fd 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; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvokeSocketConnect(SocketConnectEventArgs e) => SocketConnect?.Invoke(e); + public SingleIpFirewallEntry(string ipAddress) => MinIpAddress = IPAddress.Parse(ipAddress).ToUInt128(); + + public SingleIpFirewallEntry(IPAddress ipAddress) => MinIpAddress = ipAddress.ToUInt128(); } diff --git a/Projects/Server/Network/IPLimiter.cs b/Projects/Server/Network/IPLimiter.cs new file mode 100644 index 000000000..c0f1aec10 --- /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 6eb8d3197..1a7b4705e 100644 --- a/Projects/Server/Network/NetState/DumpNetStates.cs +++ b/Projects/Server/Network/NetState/DumpNetStates.cs @@ -30,7 +30,7 @@ public static class DumpNetStates file.WriteLine("NetState, RecvTask, SendTask, ProtocolState, ParserState"); - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { file.WriteLine($"{ns}, {ns._protocolState}, {ns._parserState}"); } diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 4c2068032..a52fbfc07 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 partial class NetState : IComparable, IValueLinkListNode, IValueLinkListNode 0) { var ns = _throttled.Dequeue(); @@ -980,7 +992,7 @@ public partial class NetState : IComparable, IValueLinkListNode 0) { @@ -1037,7 +1049,7 @@ public partial class NetState : IComparable, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode. * + *************************************************************************/ + using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Threading.Tasks; using Server.Logging; namespace Server.Network; @@ -10,14 +26,11 @@ public static class PingServer { private static readonly ILogger logger = LogFactory.GetLogger(typeof(PingServer)); - private const int MaxConnectionsPerLoop = 250; + private const int MaxConnectionsPerLoop = 128; - private const long _listenerErrorMessageDelay = 10000; // 10 seconds - private static long _nextMaximumSocketsReachedMessage; + public static int MaxQueued { get; set; } - public static int MaxConnections { get; set; } - - private static ConcurrentQueue<(UdpClient, UdpReceiveResult)> _udpResponseQueue = new(); + private static readonly ConcurrentQueue<(UdpClient, UdpReceiveResult)> _udpResponseQueue = new(); public static UdpClient[] Listeners { get; private set; } @@ -29,7 +42,7 @@ public static class PingServer { Enabled = ServerConfiguration.GetOrUpdateSetting("pingServer.enabled", true); Port = ServerConfiguration.GetSetting("pingServer.port", 12000); - MaxConnections = ServerConfiguration.GetSetting("pingServer.maxConnections", 2048); + MaxQueued = ServerConfiguration.GetSetting("pingServer.maxConnections", 2048); } public static void Start() @@ -44,6 +57,7 @@ public static class PingServer foreach (var serverIpep in ServerConfiguration.Listeners) { + var cancellationToken = Core.ClosingTokenSource.Token; var ipep = new IPEndPoint(serverIpep.Address, Port); var listener = CreateListener(ipep); @@ -62,7 +76,7 @@ public static class PingServer } listeners.Add(listener); - BeginAcceptingSockets(listener); + Task.Run(() => BeginAcceptingUdpRequest(listener), cancellationToken).ConfigureAwait(false); } foreach (var ipep in listeningAddresses) @@ -126,36 +140,21 @@ public static class PingServer return null; } - private static async void BeginAcceptingSockets(UdpClient listener) + private static async void BeginAcceptingUdpRequest(UdpClient listener) { - while (true) - { - if (!Enabled || Core.Closing) - { - return; - } + var cancellationToken = Core.ClosingTokenSource.Token; + while (!cancellationToken.IsCancellationRequested) + { try { - var result = await listener.ReceiveAsync(Core.ClosingTokenSource.Token); + var result = await listener.ReceiveAsync(cancellationToken); - if (_udpResponseQueue.Count >= MaxConnections) + if (_udpResponseQueue.Count < MaxQueued) { - var ticks = Core.TickCount; - - if (ticks - _nextMaximumSocketsReachedMessage > 0) - { - if (listener.Client.RemoteEndPoint is IPEndPoint ipep) - { - var ip = ipep.Address.ToString(); - logger.Warning("Ping Listener {Address}: Failed (Maximum connections reached)", ip); - } - - _nextMaximumSocketsReachedMessage = ticks + _listenerErrorMessageDelay; - } + _udpResponseQueue.Enqueue((listener, result)); } - _udpResponseQueue.Enqueue((listener, result)); } catch { @@ -164,7 +163,7 @@ public static class PingServer } } - private static async void SendResponse(UdpClient listener, byte[] data, IPEndPoint ipep) + private static async Task SendResponse(UdpClient listener, byte[] data, IPEndPoint ipep) { try { diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 40a4eb5a9..502fd63f3 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 + + private static long _nextMaximumSocketsReachedMessage; + private static readonly SemaphoreSlim _queueSemaphore = new(0); + private static readonly ConcurrentQueue _connectingQueue = []; + private static Thread _processConnectionsThread; // Sanity. 256 * 1024 * 4096 = ~1.3GB of ram public static int MaxConnections { get; set; } - private const long _listenerErrorMessageDelay = 10000; // 10 seconds - private static long _nextMaximumSocketsReachedMessage; - - // AccountLoginReject BadComm - private static readonly byte[] _socketRejected = { 0x82, 0xFF }; - public static IPEndPoint[] ListeningAddresses { get; private set; } public static Socket[] Listeners { get; private set; } - public static HashSet 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 class TcpServer 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 class TcpServer try { listener.Bind(ipep); - listener.Listen(32); + listener.Listen(256); return listener; } catch (SocketException se) @@ -138,74 +109,201 @@ public static class TcpServer return null; } - public static void Slice() + private static async Task BeginAcceptingSockets(Socket listener) { - int count = 0; + var cancellationToken = Core.ClosingTokenSource.Token; - while (++count <= MaxConnectionsPerLoop && _connectedQueue.TryDequeue(out var ns)) + try { - Instances.Add(ns); - ns.LogInfo($"Connected. [{Instances.Count} Online]"); + var socket = await listener.AcceptAsync(cancellationToken); + _connectingQueue.Enqueue(socket); + _queueSemaphore.Release(); } + catch(OperationCanceledException) + { + return; + } + + if (cancellationToken.IsCancellationRequested) + { + listener.Close(); + return; + } + + Task.Run(() => BeginAcceptingSockets(listener), cancellationToken).ConfigureAwait(false); } - private static async void BeginAcceptingSockets(Socket listener) + private static void ProcessConnections() { + var cancellationToken = Core.ClosingTokenSource.Token; + HashSet 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); - - if (!args.AllowConnection) - { - rejected = true; - if (socket.RemoteEndPoint is IPEndPoint ipep) - { - var ip = ipep.Address.ToString(); - NetState.TraceDisconnect("Rejected by socket event handler", ip); - } - } - - if (rejected) - { - socket.Send(_socketRejected, SocketFlags.None); - socket.Shutdown(SocketShutdown.Both); - socket.Close(); - } - else - { - var ns = new NetState(socket); - _connectedQueue.Enqueue(ns); - } + return; } - catch + catch (OperationCanceledException) { - // ignored + return; } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CloseSocket(Socket socket) + { + try + { + socket.Shutdown(SocketShutdown.Both); + } + catch + { + // ignored + } + + socket.Close(); + } + + private static void ProcessConnection(Socket socket) + { + var ipLimiter = IPLimiter.Enabled; + try + { + // Clear out any sockets that have been connecting for too long + while (_socketsConnecting.Count > 0) + { + var socketTime = _socketsConnecting.Min; // Earliest connected socket + if (Core.TickCount - socketTime.ConnectedAt <= MaximumSocketIdleDelay) + { + break; + } + + var socketToCheck = socketTime.NetState; + if (socketToCheck.Running && !socketToCheck.Seeded) + { + socketToCheck.Disconnect(null); + } + + _socketsConnecting.Remove(socketTime); + } + + var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address; + + if (NetState.Instances.Count >= MaxConnections) + { + var ticks = Core.TickCount; + + if (ticks - _nextMaximumSocketsReachedMessage > 0) + { + if (socket.RemoteEndPoint is IPEndPoint ipep) + { + var ip = ipep.Address.ToString(); + logger.Warning("{Address} Failed (Maximum connections reached)", ip); + } + + _nextMaximumSocketsReachedMessage = ticks + ListenerErrorMessageDelay; + } + + CloseSocket(socket); + return; + } + + if (ipLimiter && !IPLimiter.Verify(remoteIP)) + { + TraceDisconnect("Past IP limit threshold", remoteIP); + logger.Debug("{Address} Past IP limit threshold", remoteIP); + + CloseSocket(socket); + return; + } + + if (Firewall.IsBlocked(remoteIP)) + { + TraceDisconnect("Firewalled", remoteIP); + logger.Debug("{Address} Firewalled", remoteIP); + + CloseSocket(socket); + return; + } + + var ns = new NetState(socket); + _socketsConnecting.Add((Core.TickCount, ns)); + ConnectedQueue.Enqueue(ns); + } + catch + { + // ignored + } + } + + private static void TraceDisconnect(string reason, IPAddress ip) + { + try + { + using StreamWriter op = new StreamWriter("network-socket-disconnects.log", true); + op.WriteLine($"# {Core.Now}"); + + op.WriteLine($"Address: {ip}"); + op.WriteLine(reason); + + op.WriteLine(); + op.WriteLine(); + } + catch + { + // ignored + } + } } diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 1296a44ab..37124526c 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -158,408 +159,128 @@ public static class Utility } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint IPv4ToAddress(IPAddress ipAddress) + public static void ApplyCidrMask(ref ulong high, ref ulong low, int prefixLength, bool isMax) { - if (ipAddress.IsIPv4MappedToIPv6) + // This should never happen, a 0 CIDR is not valid + if (prefixLength == 0) { - ipAddress = ipAddress.MapToIPv4(); + high = low = ~0UL; + return; } - else if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + + if (prefixLength == 128) + { + return; + } + + if (prefixLength == 64) + { + low = 0; + return; + } + + if (prefixLength < 64) + { + int bitsToFlip = 64 - prefixLength; + ulong highMask = isMax ? ~0UL >> bitsToFlip : ~0UL << (bitsToFlip + 1); + + high = isMax ? high | highMask : high & highMask; + low = isMax ? ~0UL : 0UL; + } + else + { + int bitsToFlip = 128 - prefixLength; + ulong lowMask = isMax ? ~0UL >> (64 - bitsToFlip) : ~0UL << bitsToFlip; + + low = isMax ? low | lowMask : low & lowMask; + } + } + + // Converts an IPAddress to a UInt128 in IPv6 format + public static UInt128 ToUInt128(this IPAddress ip) + { + if (ip.AddressFamily == AddressFamily.InterNetwork && !ip.IsIPv4MappedToIPv6) + { + Span integer = stackalloc byte[4]; + return !ip.TryWriteBytes(integer, out _) + ? (UInt128)0 + : new UInt128(0, 0xFFFF00000000UL | BinaryPrimitives.ReadUInt32BigEndian(integer)); + } + + Span bytes = stackalloc byte[16]; + if (!ip.TryWriteBytes(bytes, out _)) { return 0; } - Span integer = stackalloc byte[4]; - ipAddress.TryWriteBytes(integer, out var bytesWritten); - return bytesWritten != 4 ? 0 : BinaryPrimitives.ReadUInt32BigEndian(integer); + ulong high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]); + ulong low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8)); + + return new UInt128(high, low); } - public static bool IPMatchClassC(IPAddress ip1, IPAddress ip2) + // Converts a UInt128 in IPv6 format to an IPAddress + public static IPAddress ToIpAddress(this UInt128 value, bool mapToIpv6 = false) { - var a = IPv4ToAddress(ip1); - var b = IPv4ToAddress(ip2); - - return a == 0 || b == 0 ? ip1.Equals(ip2) : (a & 0xFFFFFF) == (b & 0xFFFFFF); - } - - public static bool IPMatchCIDR(IPAddress cidrAddress, IPAddress address, int cidrLength) - { - if (cidrAddress.AddressFamily == AddressFamily.InterNetwork) + // IPv4 mapped IPv6 address + if (!mapToIpv6 && value >= 0xFFFF00000000UL && value <= 0xFFFFFFFFFFFFUL) { - if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - return false; - } - - cidrLength += 96; + var newAddress = IPAddress.HostToNetworkOrder((int)value); + return new IPAddress(unchecked((uint)newAddress)); } - cidrAddress = cidrAddress.MapToIPv6(); - address = address.MapToIPv6(); + Span bytes = stackalloc byte[16]; // 128 bits for IPv6 address + ((IBinaryInteger)value).WriteBigEndian(bytes); - cidrLength = Math.Clamp(cidrLength, 0, 128); + return new IPAddress(bytes); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 CreateCidrAddress(ReadOnlySpan bytes, int prefixLength, bool isMax) + { + ulong high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]); + ulong low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8)); + + if (prefixLength < 128) + { + ApplyCidrMask(ref high, ref low, prefixLength, isMax); + } + + return new UInt128(high, low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteMappedIPv6To(this IPAddress ipAddress, Span destination) + { + if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + ipAddress.TryWriteBytes(destination, out _); + return; + } + + destination[..8].Clear(); // Local init is off + BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(8, 4), 0xFFFF); + ipAddress.TryWriteBytes(destination.Slice(12, 4), out _); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool MatchClassC(this IPAddress ip1, IPAddress ip2) => ip1.MatchCidr(24, ip2); + + public static bool MatchCidr(this IPAddress cidrAddress, int prefixLength, IPAddress address) + { Span cidrBytes = stackalloc byte[16]; - cidrAddress.TryWriteBytes(cidrBytes, out var _); + cidrAddress.WriteMappedIPv6To(cidrBytes); - Span addrBytes = stackalloc byte[16]; - address.TryWriteBytes(addrBytes, out var _); - - var i = 0; - int offset; - - if (cidrLength < 32) + if (cidrAddress.AddressFamily != AddressFamily.InterNetworkV6) { - offset = cidrLength; - } - else - { - var index = Math.DivRem(cidrLength, 32, out offset); - while (index > 0) - { - if ( - BinaryPrimitives.ReadInt32BigEndian(cidrBytes.Slice(i, 4)) != - BinaryPrimitives.ReadInt32BigEndian(addrBytes.Slice(i, 4)) - ) - { - return false; - } - - i += 4; - --index; - } + prefixLength += 96; // 32 -> 128 } - if (offset == 0) - { - return true; - } + var min = CreateCidrAddress(cidrBytes, prefixLength, false); + var max = CreateCidrAddress(cidrBytes, prefixLength, true); + var ip = address.ToUInt128(); - var c = BinaryPrimitives.ReadInt32BigEndian(cidrBytes.Slice(i, 4)); - var a = BinaryPrimitives.ReadInt32BigEndian(addrBytes.Slice(i, 4)); - - var mask = (1 << (32 - offset)) - 1; - var min = ~mask & c; - var max = c | mask; - - return a >= min && a <= max; - } - - public static bool IsValidIP(string val) => IPMatch(val, IPAddress.Any, out var valid) || valid; - - public static bool IPMatch(string val, IPAddress ip) => IPMatch(val, ip, out _); - - public static bool IPMatch(string val, IPAddress ip, out bool valid) - { - var family = ip.AddressFamily; - var useIPv6 = family == AddressFamily.InterNetworkV6 || val.ContainsOrdinal(':'); - - ip = useIPv6 ? ip.MapToIPv6() : ip.MapToIPv4(); - - Span ipBytes = stackalloc byte[useIPv6 ? 16 : 4]; - ip.TryWriteBytes(ipBytes, out _); - - return useIPv6 ? IPv6Match(val, ipBytes, out valid) : IPv4Match(val, ipBytes, out valid); - } - - public static bool IPv4Match(ReadOnlySpan val, ReadOnlySpan ip, out bool valid) - { - var match = true; - valid = true; - var end = val.Length; - var byteIndex = 0; - var section = 0; - var number = 0; - var isRange = false; - var intBase = 10; - var endOfSection = false; - var sectionStart = 0; - - var num = ip[byteIndex++]; - - for (var i = 0; i < end; i++) - { - var chr = val[i]; - if (section >= 4) - { - valid = false; - return false; - } - - switch (chr) - { - default: - { - if (!Uri.IsHexDigit(chr)) - { - valid = false; - return false; - } - - number = number * intBase + Uri.FromHex(chr); - break; - } - case 'x': - case 'X': - { - if (i == sectionStart) - { - intBase = 16; - break; - } - - valid = false; - return false; - } - case '-': - { - if (i == sectionStart || i + 1 == end || val[i + 1] == '.') - { - valid = false; - return false; - } - - // Only allows a single range in a section - if (isRange) - { - valid = false; - return false; - } - - isRange = true; - match = match && num >= number; - number = 0; - break; - } - case '*': - { - if (i != sectionStart || i + 1 < end && val[i + 1] != '.') - { - valid = false; - return false; - } - - isRange = true; - number = 255; - break; - } - case '.': - { - endOfSection = true; - break; - } - } - - if (endOfSection || i + 1 == end) - { - if (number is < 0 or > 255) - { - valid = false; - return false; - } - - match = match && (isRange ? num <= number : number == num); - - if (++section < 4) - { - num = ip[byteIndex++]; - } - - intBase = 10; - number = 0; - endOfSection = false; - sectionStart = i + 1; - isRange = false; - } - } - - return match; - } - - public static bool IPv6Match(ReadOnlySpan val, ReadOnlySpan ip, out bool valid) - { - valid = true; - - // Start must be two `::` or a number - if (val[0] == ':' && val[1] != ':') - { - valid = false; - return false; - } - - var match = true; - var end = val.Length; - var byteIndex = 2; - var section = 0; - var number = 0; - var isRange = false; - var endOfSection = false; - var sectionStart = 0; - var hasCompressor = false; - - var num = BinaryPrimitives.ReadUInt16BigEndian(ip[..2]); - - for (int i = 0; i < end; i++) - { - if (section > 7) - { - valid = false; - return false; - } - - var chr = val[i]; - // We are starting a new sequence, check the previous one then continue - switch (chr) - { - default: - { - if (!Uri.IsHexDigit(chr)) - { - valid = false; - return false; - } - - number = number * 16 + Uri.FromHex(chr); - break; - } - case '?': - { - logger.Debug("IP Match '?' character is not supported."); - valid = false; - return false; - } - // Range - case '-': - { - if (i == sectionStart || i + 1 == end || val[i + 1] == ':') - { - valid = false; - return false; - } - - // Only allows a single range in a section - if (isRange) - { - valid = false; - return false; - } - - isRange = true; - - // Check low part of the range - match = match && num >= number; - number = 0; - break; - } - // Wild section - case '*': - { - if (i != sectionStart || i + 1 < end && val[i + 1] != ':') - { - valid = false; - return false; - } - - isRange = true; - number = 65535; - break; - } - case ':': - { - endOfSection = true; - break; - } - } - - if (!endOfSection && i + 1 != end) - { - continue; - } - - if (++i == end || val[i] != ':' || section > 0) - { - match = match && (isRange ? num <= number : number == num); - - // IPv4 matching at the end - if (section == 6 && num == 0xFFFF) - { - var ipv4 = val[(i + 1)..]; - if (ipv4.Contains('.')) - { - return IPv4Match(ipv4, ip[^4..], out valid); - } - } - - if (i == end) - { - break; - } - - num = BinaryPrimitives.ReadUInt16BigEndian(ip.Slice(byteIndex, 2)); - byteIndex += 2; - - ++section; - } - - if (i < end && val[i] == ':') - { - if (hasCompressor) - { - valid = false; - return false; - } - - int newSection; - - if (i + 1 < end) - { - var remainingColons = val[(i + 1)..].Count(':'); - // double colon must be at least 2 sections - // we need at least 1 section remaining out of 8 - // This means 8 - 2 would be 6 sections (5 colons) - newSection = section + 2 + (5 - remainingColons); - if (newSection > 7) - { - valid = false; - return false; - } - } - else - { - newSection = 7; - } - - var zeroEnd = (newSection + 1) * 2; - do - { - if (match) - { - if (num != 0) - { - match = false; - } - - num = BinaryPrimitives.ReadUInt16BigEndian(ip.Slice(byteIndex, 2)); - } - - byteIndex += 2; - } while (byteIndex < zeroEnd); - - section = newSection; - hasCompressor = true; - } - else - { - i--; - } - - number = 0; - endOfSection = false; - sectionStart = i + 1; - isRange = false; - } - - return match; + return ip >= min && ip <= max; } public static string FixHtml(string str) diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index ed2a37ed1..2344baad4 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -103,7 +103,7 @@ public static class World 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 class World 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 79c1fd0dd..000000000 --- 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 4522e558c..000000000 --- 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 abd5c4286..729fd6766 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -136,7 +136,7 @@ public static class AccountHandler { 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 @@ public static class AccountHandler 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 class AccountHandler 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 88ea0471e..000000000 --- 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 49c72af89..000000000 --- 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 b780ae390..e70533aa5 100644 --- a/Projects/UOContent/Commands/Generic/Commands/Commands.cs +++ b/Projects/UOContent/Commands/Generic/Commands/Commands.cs @@ -1137,7 +1137,7 @@ namespace Server.Commands.Generic 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 39f06d8ab..faf39175e 100644 --- a/Projects/UOContent/Commands/Generic/Implementors/IPAddressCommandImplementor.cs +++ b/Projects/UOContent/Commands/Generic/Implementors/IPAddressCommandImplementor.cs @@ -38,7 +38,7 @@ namespace Server.Commands.Generic 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 1120b6f0f..b3144f6a8 100644 --- a/Projects/UOContent/Commands/Generic/Implementors/OnlineCommandImplementor.cs +++ b/Projects/UOContent/Commands/Generic/Implementors/OnlineCommandImplementor.cs @@ -36,7 +36,7 @@ namespace Server.Commands.Generic 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 c62957329..53cd5152e 100644 --- a/Projects/UOContent/Commands/Handlers.cs +++ b/Projects/UOContent/Commands/Handlers.cs @@ -753,7 +753,7 @@ namespace Server.Commands 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 @@ namespace Server.Commands [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 c64290961..4c2cc8668 100644 --- a/Projects/UOContent/Engines/Help/PageQueue.cs +++ b/Projects/UOContent/Engines/Help/PageQueue.cs @@ -231,7 +231,7 @@ namespace Server.Engines.Help 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 8fca4fff6..810dc22e8 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -171,10 +171,10 @@ namespace Server.Gumps 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 @@ namespace Server.Gumps { 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 @@ namespace Server.Gumps { AddFirewallHeader(); - m_List ??= Firewall.Set.ToList(); + m_List ??= AdminFirewall.Set.ToList(); AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address"); @@ -1275,7 +1275,7 @@ namespace Server.Gumps { AddFirewallHeader(); - if (state is not Firewall.IFirewallEntry firewallEntry) + if (state is not IFirewallEntry firewallEntry) { break; } @@ -1804,7 +1804,7 @@ namespace Server.Gumps { 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 @@ namespace Server.Gumps if (okay) { - Firewall.Add(toFirewall); + AdminFirewall.Add(toFirewall); notice = $"{toFirewall} : Added to firewall."; } @@ -2427,7 +2427,7 @@ namespace Server.Gumps { var count = 0; - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var a = ns.Account; @@ -2533,7 +2533,7 @@ namespace Server.Gumps } else { - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { bool isMatch; @@ -3626,7 +3626,7 @@ namespace Server.Gumps } else { - foreach (var check in Firewall.Set) + foreach (var check in AdminFirewall.Set) { var checkStr = check.ToString(); @@ -3692,42 +3692,47 @@ namespace Server.Gumps 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 @@ namespace Server.Gumps } 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 8f925474c..0833afe3b 100644 --- a/Projects/UOContent/Gumps/WhoGump.cs +++ b/Projects/UOContent/Gumps/WhoGump.cs @@ -60,7 +60,7 @@ namespace Server.Gumps 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 9b6d38755..14761bbcd 100644 --- a/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs +++ b/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs @@ -289,7 +289,7 @@ public partial class HouseRaffleStone : Item 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 000000000..4f0ab02ae --- /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 592cc724b..a775297b8 100644 --- a/Projects/UOContent/Misc/CrashGuard.cs +++ b/Projects/UOContent/Misc/CrashGuard.cs @@ -178,11 +178,11 @@ namespace Server.Misc 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 7174db5dd..8cb1c21d2 100644 --- a/Projects/UOContent/Misc/FoodDecay.cs +++ b/Projects/UOContent/Misc/FoodDecay.cs @@ -21,7 +21,7 @@ namespace Server.Misc 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 1b592d8e8..301952f9f 100644 --- a/Projects/UOContent/Misc/LightCycle.cs +++ b/Projects/UOContent/Misc/LightCycle.cs @@ -21,7 +21,7 @@ namespace Server { m_LevelOverride = value; - foreach (var ns in TcpServer.Instances) + foreach (var ns in NetState.Instances) { var m = ns.Mobile; @@ -98,7 +98,7 @@ namespace Server 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 e47acbcdf..0ea213cd0 100644 --- a/Projects/UOContent/Misc/LoginStats.cs +++ b/Projects/UOContent/Misc/LoginStats.cs @@ -12,7 +12,7 @@ namespace Server.Misc 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 1b60f14d9..e208f7c9b 100644 --- a/Projects/UOContent/Misc/ServerList.cs +++ b/Projects/UOContent/Misc/ServerList.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; using Server.Logging; +using Server.Network; namespace Server.Misc { @@ -153,18 +154,39 @@ namespace Server.Misc 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 16d34c5ff..f12e4a39c 100644 --- a/Projects/UOContent/Misc/ShardPoller.cs +++ b/Projects/UOContent/Misc/ShardPoller.cs @@ -319,7 +319,7 @@ namespace Server.Misc 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 f02977ff2..a29b7e985 100644 --- a/Projects/UOContent/Misc/Weather.cs +++ b/Projects/UOContent/Misc/Weather.cs @@ -345,7 +345,7 @@ namespace Server.Misc 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 439b228d8..62970c0ac 100644 --- a/Projects/UOContent/Network/UOGateway.cs +++ b/Projects/UOContent/Network/UOGateway.cs @@ -37,7 +37,7 @@ namespace Server.Network { 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 @@ namespace Server.Network 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 4ed56ec73..1477d35be 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" }