Skip to content

Commit

Permalink
feat: Moves TcpServer to another thread. Rewrites Firewall (#1660)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
kamronbatman authored Jan 20, 2024
1 parent 5f3de65 commit 4cd668e
Show file tree
Hide file tree
Showing 38 changed files with 1,085 additions and 998 deletions.
35 changes: 35 additions & 0 deletions Projects/Server.Tests/Tests/Network/Firewall/FirewallEntryTests.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
53 changes: 14 additions & 39 deletions Projects/Server.Tests/Tests/Utility/IPAddressTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> 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);
}
}
}
1 change: 0 additions & 1 deletion Projects/Server/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,6 @@ public static void RunEventLoop()
Timer.Slice(_tickCount);

// Handle networking
TcpServer.Slice();
NetState.Slice();
PingServer.Slice();

Expand Down
71 changes: 71 additions & 0 deletions Projects/Server/Network/Firewall/BaseFirewallEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: [email protected] *
* File: BaseFirewallEntry.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/

using System;
using System.Net;
using System.Runtime.CompilerServices;

namespace Server.Network;

public abstract class BaseFirewallEntry : IFirewallEntry, ISpanFormattable
{
public abstract UInt128 MinIpAddress { get; }
public abstract UInt128 MaxIpAddress { get; }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsBlocked(IPAddress address) => IsBlocked(address.ToUInt128());

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsBlocked(UInt128 address) => address >= MinIpAddress && address <= MaxIpAddress;

public override string ToString() =>
MinIpAddress == MaxIpAddress ? MinIpAddress.ToIpAddress().ToString()
: $"{MinIpAddress.ToIpAddress()}-{MaxIpAddress.ToIpAddress()}";

public string ToString(string? format, IFormatProvider? formatProvider) =>
// format and provider are explicitly ignored
ToString();

public bool TryFormat(
Span<char> destination,
out int charsWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider
)
{
if (!((ISpanFormattable)MinIpAddress.ToIpAddress()).TryFormat(destination, out charsWritten, format, provider))
{
return false;
}

if (MinIpAddress == MaxIpAddress)
{
return true;
}

// Range
destination[charsWritten++] = '-';

var total = charsWritten;

if (!((ISpanFormattable)MaxIpAddress.ToIpAddress()).TryFormat(destination[charsWritten..], out charsWritten, format, provider))
{
return false;
}

charsWritten += total;
return true;
}
}
75 changes: 75 additions & 0 deletions Projects/Server/Network/Firewall/CidrFirewallEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: [email protected] *
* File: CidrFirewallEntry.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/

using System;
using System.Net;
using System.Net.Sockets;

namespace Server.Network;

public class CidrFirewallEntry : BaseFirewallEntry
{
public override UInt128 MinIpAddress { get; }
public override UInt128 MaxIpAddress { get; }

public CidrFirewallEntry(string ipAddressOrCidr)
: this(ParseIPAddress(ipAddressOrCidr, out var prefixLength), prefixLength)
{
}

public CidrFirewallEntry(IPAddress minAddress, IPAddress maxAddress)
{
MinIpAddress = minAddress.ToUInt128();
MaxIpAddress = maxAddress.ToUInt128();
}

public CidrFirewallEntry(IPAddress ipAddress, int prefixLength)
{
Span<byte> bytes = stackalloc byte[16];

if (ipAddress.AddressFamily != AddressFamily.InterNetworkV6)
{
prefixLength += 96; // 32 -> 128
}

ipAddress.WriteMappedIPv6To(bytes);

MinIpAddress = Utility.CreateCidrAddress(bytes, prefixLength, false);
MaxIpAddress = Utility.CreateCidrAddress(bytes, prefixLength, true);
}

private static IPAddress ParseIPAddress(ReadOnlySpan<char> ipString, out int prefixLength)
{
int slashIndex = ipString.IndexOf('/');
var ipAddress = IPAddress.Parse(slashIndex > -1 ? ipString[..slashIndex] : ipString);
var maxPrefixLength = ipAddress.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;

if (slashIndex == -1)
{
prefixLength = maxPrefixLength;
}
else
{
var prefixPart = ipString[(slashIndex + 1)..];

if (!int.TryParse(prefixPart, out prefixLength) || prefixLength < 0 || prefixLength > maxPrefixLength)
{
throw new ArgumentException("Invalid prefix length.");
}
}

return ipAddress;
}
}
Loading

0 comments on commit 4cd668e

Please sign in to comment.