refactor(network): move Firewall to UOContent; core keeps only the filter seam

Core now owns the question -- "should this socket be denied?" -- and none of the
answers. The firewall was the last implementation left in core, and the reasons
to keep it did not survive scrutiny: it is not extended downstream, and a shard
running bare core has no way to populate it anyway, since the admin gump and
the commands that mutate it are both content. Larger shards front the server
with an upstream proxy or edge scrubbing and never use it; it survives as the
fallback an admin reaches for over a single player, which is squarely content's
concern.

Nothing about the firewall changes for operators: same Server.Network namespace,
same Configuration/firewall.json, same gump and commands, same legacy .cfg
migration. It reaches the accept path through ConnectionFilters like any other
filter, and registers itself first because an empty set is the cheapest gate.

Untangling core from the firewall entry types first:

- NetworkUtilities built its reserved-network tables out of CidrFirewallEntry,
  which made core depend on the firewall for something with nothing to do with
  banning. Those are constant CIDR blocks answering "is this address in one of
  these ranges?", so they are now a SortedRangeIndex<UInt128> -- the same
  primitive the firewall and blocklist already share. Same semantics, same
  public API, one linear scan replaced by a binary search.
- The CIDR -> normalized range parse those tables needed is now
  IPAddressUtility.TryParseCidrRange, and CidrFirewallEntry drops its private
  copy of that logic in favor of it.

Core no longer references IFirewallEntry or Firewall anywhere. 1344 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-07-25 01:18:42 -07:00
parent 2be79d054a
commit 50c8287c7e
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
14 changed files with 98 additions and 57 deletions

View file

@ -1,35 +0,0 @@
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());
}
}

View file

@ -1,71 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: FirewallPersistenceTests.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 Server.Network;
using Xunit;
namespace Server.Tests.Network.Firewall;
[Collection("Sequential Server Tests")]
public class FirewallPersistenceTests
{
[Fact]
public void ToSettings_RoundTrips_PermanentAndTtl()
{
Server.Network.Firewall.ResetForTesting();
Server.Network.Firewall.Add(new SingleIpFirewallEntry(IPAddress.Parse("1.2.3.4")));
Server.Network.Firewall.Add(new SingleIpFirewallEntry(IPAddress.Parse("2.2.2.2")), TimeSpan.FromHours(1));
var settings = Server.Network.Firewall.ToSettings();
Server.Network.Firewall.ResetForTesting();
Server.Network.Firewall.LoadFrom(settings);
Assert.True(Server.Network.Firewall.IsBlocked(IPAddress.Parse("1.2.3.4")));
Assert.True(Server.Network.Firewall.IsBlocked(IPAddress.Parse("2.2.2.2")));
}
[Fact]
public void LoadFrom_SkipsAlreadyExpired()
{
Server.Network.Firewall.ResetForTesting();
var settings = new FirewallSettings
{
Entries =
[
new FirewallEntryRecord { Value = "9.9.9.9", Expires = DateTime.UtcNow.AddHours(-1) }
]
};
Server.Network.Firewall.LoadFrom(settings);
Assert.False(Server.Network.Firewall.IsBlocked(IPAddress.Parse("9.9.9.9")));
}
[Fact]
public void ToSettings_OmitsExpiryForPermanent()
{
Server.Network.Firewall.ResetForTesting();
Server.Network.Firewall.Add(new SingleIpFirewallEntry(IPAddress.Parse("1.2.3.4")));
var settings = Server.Network.Firewall.ToSettings();
Assert.Single(settings.Entries);
Assert.Null(settings.Entries[0].Expires);
Assert.Equal("1.2.3.4", settings.Entries[0].Value);
}
}

View file

@ -1,89 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: FirewallTests.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 Server.Network;
using Xunit;
namespace Server.Tests.Network.Firewall;
[Collection("Sequential Server Tests")]
public class FirewallTests
{
private static IPAddress Ip(string s) => IPAddress.Parse(s);
[Fact]
public void Add_ThenIsBlocked_SingleIp()
{
Server.Network.Firewall.ResetForTesting();
Assert.True(Server.Network.Firewall.Add(new SingleIpFirewallEntry(Ip("1.2.3.4"))));
Assert.True(Server.Network.Firewall.IsBlocked(Ip("1.2.3.4")));
Assert.False(Server.Network.Firewall.IsBlocked(Ip("1.2.3.5")));
}
[Fact]
public void Add_Range_BlocksInside_NotOutside()
{
Server.Network.Firewall.ResetForTesting();
Server.Network.Firewall.Add(new CidrFirewallEntry(Ip("10.0.0.0"), Ip("10.0.0.255")));
Assert.True(Server.Network.Firewall.IsBlocked(Ip("10.0.0.7")));
Assert.False(Server.Network.Firewall.IsBlocked(Ip("10.0.1.0")));
}
[Fact]
public void Remove_Unblocks()
{
Server.Network.Firewall.ResetForTesting();
var entry = new SingleIpFirewallEntry(Ip("1.2.3.4"));
Server.Network.Firewall.Add(entry);
Assert.True(Server.Network.Firewall.Remove(entry));
Assert.False(Server.Network.Firewall.IsBlocked(Ip("1.2.3.4")));
}
[Fact]
public void ExpireEntries_RemovesExpired_KeepsPermanent()
{
Server.Network.Firewall.ResetForTesting();
var permanent = new SingleIpFirewallEntry(Ip("1.1.1.1"));
var temporary = new SingleIpFirewallEntry(Ip("2.2.2.2"));
Server.Network.Firewall.Add(permanent); // no ttl
Server.Network.Firewall.Add(temporary, TimeSpan.FromMilliseconds(50)); // ttl
Server.Network.Firewall.ExpireEntries(Core.TickCount + 100); // past the ttl
Assert.True(Server.Network.Firewall.IsBlocked(Ip("1.1.1.1")));
Assert.False(Server.Network.Firewall.IsBlocked(Ip("2.2.2.2")));
}
[Fact]
public void ToFirewallEntry_ParsesForms()
{
Assert.IsType<SingleIpFirewallEntry>(Server.Network.Firewall.ToFirewallEntry("1.2.3.4"));
Assert.IsType<CidrFirewallEntry>(Server.Network.Firewall.ToFirewallEntry("10.0.0.0/24"));
Assert.IsType<CidrFirewallEntry>(Server.Network.Firewall.ToFirewallEntry("10.0.0.0-10.0.0.255"));
Assert.Null(Server.Network.Firewall.ToFirewallEntry("not-an-ip"));
}
[Fact]
public void ReadFirewallSet_SurfacesAddedEntries()
{
Server.Network.Firewall.ResetForTesting();
var entry = new SingleIpFirewallEntry(Ip("1.2.3.4"));
Server.Network.Firewall.Add(entry);
Server.Network.Firewall.ReadFirewallSet(set => Assert.Contains(entry, set));
}
}