refactor(network): collapse Firewall to a single-threaded persisted store
Merge the old Firewall engine, AdminFirewall (parsing/persistence), and the runtime TTL sweep into one single-threaded store: no locks/version-counter, main-thread TTL expiry, and persistence to Configuration/firewall.json (with a one-time firewall.cfg migration). Lookups go through a shared, coalesced SortedRangeIndex<T> (binary search); IP conversion/parse helpers are collected in IPAddressUtility. Firewall keeps IFirewallEntry for admin/gump/persistence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
858c1d18bc
commit
4ec488f59b
8 changed files with 825 additions and 383 deletions
|
|
@ -0,0 +1,71 @@
|
|||
/*************************************************************************
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +1,89 @@
|
|||
/*************************************************************************
|
||||
* 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 System.Threading.Tasks;
|
||||
using Server.Network;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests;
|
||||
namespace Server.Tests.Network.Firewall;
|
||||
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class FirewallTests
|
||||
{
|
||||
private static IPAddress Ip(string s) => IPAddress.Parse(s);
|
||||
|
||||
[Fact]
|
||||
public void Firewall_BlocksIPAddress_WhenAdded()
|
||||
public void Add_ThenIsBlocked_SingleIp()
|
||||
{
|
||||
var ip = IPAddress.Parse("192.168.1.1");
|
||||
var entry = new SingleIpFirewallEntry("192.168.1.1");
|
||||
|
||||
Assert.False(Firewall.IsBlocked(ip));
|
||||
|
||||
Firewall.Add(entry);
|
||||
|
||||
Assert.True(Firewall.IsBlocked(ip));
|
||||
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 Firewall_DoesNotBlockIPAddress_WhenNotAdded()
|
||||
public void Add_Range_BlocksInside_NotOutside()
|
||||
{
|
||||
var ip = IPAddress.Parse("192.168.1.2");
|
||||
Assert.False(Firewall.IsBlocked(ip));
|
||||
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 Firewall_StopsBlockingIPAddress_WhenRemoved()
|
||||
public void Remove_Unblocks()
|
||||
{
|
||||
var ip = IPAddress.Parse("192.168.1.3");
|
||||
var entry = new SingleIpFirewallEntry("192.168.1.3");
|
||||
|
||||
Firewall.Add(entry);
|
||||
Assert.True(Firewall.IsBlocked(ip));
|
||||
|
||||
Firewall.Remove(entry);
|
||||
Assert.False(Firewall.IsBlocked(ip));
|
||||
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 Firewall_BlocksIPRange()
|
||||
public void ExpireEntries_RemovesExpired_KeepsPermanent()
|
||||
{
|
||||
var entry = new CidrFirewallEntry(IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.5"));
|
||||
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
|
||||
|
||||
Firewall.Add(entry);
|
||||
Server.Network.Firewall.ExpireEntries(Core.TickCount + 100); // past the ttl
|
||||
|
||||
Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.1")));
|
||||
Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.3")));
|
||||
Assert.True(Firewall.IsBlocked(IPAddress.Parse("10.0.0.5")));
|
||||
|
||||
Assert.False(Firewall.IsBlocked(IPAddress.Parse("10.0.0.6")));
|
||||
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 Firewall_CacheInvalidation_WorksOnUpdate()
|
||||
public void ToFirewallEntry_ParsesForms()
|
||||
{
|
||||
var ip = IPAddress.Parse("192.168.1.10");
|
||||
var entry = new SingleIpFirewallEntry("192.168.1.10");
|
||||
|
||||
Firewall.Add(entry);
|
||||
Assert.True(Firewall.IsBlocked(ip));
|
||||
|
||||
Firewall.Remove(entry);
|
||||
Assert.False(Firewall.IsBlocked(ip));
|
||||
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 Firewall_ReadsFirewallSetCorrectly()
|
||||
public void ReadFirewallSet_SurfacesAddedEntries()
|
||||
{
|
||||
var entry = new SingleIpFirewallEntry("172.16.0.1");
|
||||
Firewall.Add(entry);
|
||||
Server.Network.Firewall.ResetForTesting();
|
||||
var entry = new SingleIpFirewallEntry(Ip("1.2.3.4"));
|
||||
Server.Network.Firewall.Add(entry);
|
||||
|
||||
var found = false;
|
||||
Firewall.ReadFirewallSet(set =>
|
||||
{
|
||||
found = set.Contains(entry);
|
||||
});
|
||||
|
||||
Assert.True(found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Firewall_IsThreadSafe()
|
||||
{
|
||||
var testIps = new IPAddress[256];
|
||||
for (var i = 0; i <= 255; i++)
|
||||
{
|
||||
testIps[i] = IPAddress.Parse($"192.168.0.{i}");
|
||||
}
|
||||
|
||||
var entry = new CidrFirewallEntry(IPAddress.Parse("192.168.0.1"), IPAddress.Parse("192.168.0.255"));
|
||||
Firewall.Add(entry);
|
||||
|
||||
Parallel.ForEach(testIps, ip =>
|
||||
{
|
||||
var shouldBlock = int.Parse(ip.ToString().Split('.')[3]) is > 0;
|
||||
Assert.Equal(shouldBlock, Firewall.IsBlocked(ip));
|
||||
});
|
||||
|
||||
Firewall.Remove(entry);
|
||||
|
||||
Parallel.ForEach(testIps, ip =>
|
||||
{
|
||||
Assert.False(Firewall.IsBlocked(ip));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Firewall_DoesNotThrowWhenRemovingNonExistentEntry()
|
||||
{
|
||||
var entry = new SingleIpFirewallEntry("203.0.113.5");
|
||||
Assert.False(Firewall.Remove(entry));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Firewall_CacheHandlesMultipleUpdates()
|
||||
{
|
||||
var ip = IPAddress.Parse("192.168.1.20");
|
||||
var entry = new SingleIpFirewallEntry("192.168.1.20");
|
||||
|
||||
Firewall.Add(entry);
|
||||
Assert.True(Firewall.IsBlocked(ip));
|
||||
|
||||
Firewall.Remove(entry);
|
||||
Assert.False(Firewall.IsBlocked(ip));
|
||||
|
||||
Firewall.Add(entry);
|
||||
Assert.True(Firewall.IsBlocked(ip));
|
||||
|
||||
Firewall.Remove(entry);
|
||||
Assert.False(Firewall.IsBlocked(ip));
|
||||
Server.Network.Firewall.ReadFirewallSet(set => Assert.Contains(entry, set));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
134
Projects/Server/Collections/SortedRangeIndex.cs
Normal file
134
Projects/Server/Collections/SortedRangeIndex.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: SortedRangeIndex.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.Numerics;
|
||||
|
||||
namespace Server.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable, allocation-lean membership index over a set of inclusive integer ranges. Ranges are
|
||||
/// stored as two parallel arrays (<c>_mins</c>/<c>_maxs</c>) sorted by minimum and coalesced into
|
||||
/// disjoint runs, so a single binary search decides membership. Coalescing is required for
|
||||
/// correctness: <see cref="Contains"/> only inspects the rightmost run whose minimum is <= the
|
||||
/// value, which is only sound when the runs never overlap or nest.
|
||||
/// </summary>
|
||||
public sealed class SortedRangeIndex<T> where T : IBinaryInteger<T>
|
||||
{
|
||||
public static readonly SortedRangeIndex<T> Empty = new([], []);
|
||||
|
||||
/// <summary>An inclusive <c>[Min, Max]</c> range. Singles are represented as <c>Min == Max</c>.</summary>
|
||||
public readonly record struct Range(T Min, T Max);
|
||||
|
||||
/// <summary>Orders ranges ascending by minimum; the coalescing pass in <see cref="Build"/> requires this.</summary>
|
||||
public static readonly Comparison<Range> ByMin = static (a, b) => a.Min.CompareTo(b.Min);
|
||||
|
||||
private readonly T[] _mins;
|
||||
private readonly T[] _maxs;
|
||||
|
||||
private SortedRangeIndex(T[] mins, T[] maxs)
|
||||
{
|
||||
_mins = mins;
|
||||
_maxs = maxs;
|
||||
}
|
||||
|
||||
public int Count => _mins.Length;
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="value"/> falls in any range. Binary-searches for the rightmost run
|
||||
/// whose minimum is <= the value, then tests that value against that run's maximum.
|
||||
/// </summary>
|
||||
public bool Contains(T value)
|
||||
{
|
||||
var lo = 0;
|
||||
var hi = _mins.Length - 1;
|
||||
var found = -1;
|
||||
while (lo <= hi)
|
||||
{
|
||||
var mid = (lo + hi) >> 1;
|
||||
if (_mins[mid] <= value)
|
||||
{
|
||||
found = mid;
|
||||
lo = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return found >= 0 && value <= _maxs[found];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an index from ranges that are already sorted ascending by <see cref="Range.Min"/> (sort
|
||||
/// the source with <see cref="ByMin"/> first). Overlapping and nested ranges are merged into disjoint
|
||||
/// runs. Two passes over the (pooled) input keep the run count exact so only the two final arrays are
|
||||
/// heap-allocated: pass one counts the runs, pass two fills the exact-size arrays.
|
||||
/// </summary>
|
||||
public static SortedRangeIndex<T> Build(ReadOnlySpan<Range> sortedByMin)
|
||||
{
|
||||
if (sortedByMin.IsEmpty)
|
||||
{
|
||||
return Empty;
|
||||
}
|
||||
|
||||
// Pass 1: count the disjoint runs so the final arrays can be sized exactly.
|
||||
var runs = 1;
|
||||
var curMax = sortedByMin[0].Max;
|
||||
for (var i = 1; i < sortedByMin.Length; i++)
|
||||
{
|
||||
var r = sortedByMin[i];
|
||||
if (r.Min <= curMax)
|
||||
{
|
||||
if (r.Max > curMax)
|
||||
{
|
||||
curMax = r.Max;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
runs++;
|
||||
curMax = r.Max;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: write the coalesced runs into the exact-size final arrays.
|
||||
var mins = new T[runs];
|
||||
var maxs = new T[runs];
|
||||
var w = 0;
|
||||
mins[0] = sortedByMin[0].Min;
|
||||
maxs[0] = sortedByMin[0].Max;
|
||||
for (var i = 1; i < sortedByMin.Length; i++)
|
||||
{
|
||||
var r = sortedByMin[i];
|
||||
if (r.Min <= maxs[w])
|
||||
{
|
||||
if (r.Max > maxs[w])
|
||||
{
|
||||
maxs[w] = r.Max;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
w++;
|
||||
mins[w] = r.Min;
|
||||
maxs[w] = r.Max;
|
||||
}
|
||||
}
|
||||
|
||||
return new SortedRangeIndex<T>(mins, maxs);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,120 +14,118 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Server.Collections;
|
||||
using Server.Json;
|
||||
using Server.Logging;
|
||||
|
||||
namespace Server.Network;
|
||||
|
||||
public static class Firewall
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static InternalValidationEntry _validationEntry;
|
||||
private static readonly ConcurrentDictionary<IPAddress, int> _isBlockedCache = [];
|
||||
private static readonly ReaderWriterLockSlim _firewallLock = new(LockRecursionPolicy.NoRecursion);
|
||||
// Single-threaded: the accept path, admin gump/command, TTL expiry timer, and boot load all run on
|
||||
// the main game loop. No locks, caches, or version counters are needed. See the ban-channel design doc.
|
||||
// _entries is the authoritative store (gump/persistence/TTL/command all work against it); _index is a
|
||||
// derived, rebuild-on-demand SortedRangeIndex used only for the accept-path IsBlocked lookup, shared
|
||||
// with the same sorted-range binary-search primitive the blocklist uses (see BlocklistSnapshot).
|
||||
private static readonly List<IFirewallEntry> _entries = [];
|
||||
|
||||
private static int _firewallVersion;
|
||||
private static readonly SortedSet<IFirewallEntry> _firewallSet = [];
|
||||
// Entries with a TTL: entry -> absolute expiry tick (Core.TickCount). Permanent entries are absent.
|
||||
private static readonly Dictionary<IFirewallEntry, long> _expiring = new();
|
||||
|
||||
public static int FirewallSetCount => _firewallSet.Count;
|
||||
private static SortedRangeIndex<UInt128> _index = SortedRangeIndex<UInt128>.Empty;
|
||||
private static bool _indexDirty;
|
||||
|
||||
public static void ReadFirewallSet(Action<IReadOnlySet<IFirewallEntry>> callback)
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Firewall));
|
||||
private const string _path = "Configuration/firewall.json";
|
||||
private const string _legacyPath = "firewall.cfg";
|
||||
private static bool _dirty;
|
||||
private static bool _configured;
|
||||
|
||||
public static int FirewallSetCount => _entries.Count;
|
||||
|
||||
public static void ReadFirewallSet(Action<IReadOnlyCollection<IFirewallEntry>> callback) => callback(_entries);
|
||||
|
||||
public static bool IsBlocked(IPAddress address)
|
||||
{
|
||||
_firewallLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
callback(_firewallSet);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_firewallLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsBlocked(IPAddress address)
|
||||
{
|
||||
if (_isBlockedCache.TryGetValue(address, out var blockVersion) && blockVersion == _firewallVersion)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_validationEntry == null)
|
||||
{
|
||||
_validationEntry = new InternalValidationEntry(address);
|
||||
}
|
||||
else
|
||||
{
|
||||
_validationEntry.Address = address;
|
||||
}
|
||||
|
||||
if (CheckBlocked(_validationEntry))
|
||||
{
|
||||
_isBlockedCache[address] = _firewallVersion;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckBlocked(IFirewallEntry validationEntry)
|
||||
{
|
||||
if (_firewallSet.Count == 0)
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_firewallLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var min = _firewallSet.Min;
|
||||
if (validationEntry.CompareTo(min) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all entries that are lower than our validation entry
|
||||
var view = _firewallSet.GetViewBetween(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))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return view.Max?.IsBlocked(validationEntry.MinIpAddress) == true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_firewallLock.ExitReadLock();
|
||||
}
|
||||
EnsureIndex();
|
||||
return _index.Contains(address.ToUInt128());
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Add(IFirewallEntry firewallEntry)
|
||||
// Rebuilds the derived lookup index from the authoritative _entries list, but only when entries have
|
||||
// changed since the last build. Runs on the main game loop, so the pooled build buffer is single-threaded
|
||||
// (mt: false); only the two final SortedRangeIndex arrays are heap-allocated.
|
||||
private static void EnsureIndex()
|
||||
{
|
||||
_firewallLock.EnterWriteLock();
|
||||
try
|
||||
if (!_indexDirty)
|
||||
{
|
||||
if (_firewallSet.Add(firewallEntry))
|
||||
return;
|
||||
}
|
||||
|
||||
using var ranges = PooledRefList<SortedRangeIndex<UInt128>.Range>.Create(_entries.Count, mt: false);
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
ranges.Add(new SortedRangeIndex<UInt128>.Range(entry.MinIpAddress, entry.MaxIpAddress));
|
||||
}
|
||||
|
||||
ranges.Sort(SortedRangeIndex<UInt128>.ByMin);
|
||||
_index = SortedRangeIndex<UInt128>.Build(ranges.AsSpan());
|
||||
_indexDirty = false;
|
||||
}
|
||||
|
||||
public static bool Add(IFirewallEntry firewallEntry) => Add(firewallEntry, TimeSpan.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an entry. <paramref name="ttl"/> <= <see cref="TimeSpan.Zero"/> means permanent. Returns false
|
||||
/// if the entry was already present.
|
||||
/// </summary>
|
||||
// Indexed scan (no closure allocation); firewall lists are small, so O(n) is negligible and this
|
||||
// stays off the hot path (Add/Remove are admin/boot actions, not the accept path).
|
||||
private static int IndexOfEntry(IFirewallEntry entry)
|
||||
{
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
if (_entries[i].CompareTo(entry) == 0)
|
||||
{
|
||||
Interlocked.Increment(ref _firewallVersion); // Update version
|
||||
return true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static bool Add(IFirewallEntry firewallEntry, TimeSpan ttl, bool persist = true)
|
||||
{
|
||||
if (firewallEntry == null || IndexOfEntry(firewallEntry) >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
|
||||
_entries.Add(firewallEntry);
|
||||
|
||||
if (ttl > TimeSpan.Zero)
|
||||
{
|
||||
_firewallLock.ExitWriteLock();
|
||||
_expiring[firewallEntry] = Core.TickCount + (long)ttl.TotalMilliseconds;
|
||||
}
|
||||
|
||||
_indexDirty = true;
|
||||
|
||||
if (persist)
|
||||
{
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Remove(IFirewallEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
|
|
@ -135,34 +133,267 @@ public static class Firewall
|
|||
return false;
|
||||
}
|
||||
|
||||
_firewallLock.EnterWriteLock();
|
||||
try
|
||||
var index = IndexOfEntry(entry);
|
||||
if (index < 0)
|
||||
{
|
||||
if (_firewallSet.Remove(entry))
|
||||
{
|
||||
Interlocked.Increment(ref _firewallVersion); // Update version
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
|
||||
// Remove the stored instance from _expiring (not the passed reference), so a value-equal
|
||||
// entry created elsewhere still clears the TTL bookkeeping.
|
||||
var stored = _entries[index];
|
||||
_entries.RemoveAt(index);
|
||||
_expiring.Remove(stored);
|
||||
_indexDirty = true;
|
||||
MarkDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes every entry whose TTL has elapsed. Called from the main-thread maintenance timer (Task 2).
|
||||
/// </summary>
|
||||
internal static void ExpireEntries(long nowTicks)
|
||||
{
|
||||
if (_expiring.Count == 0)
|
||||
{
|
||||
_firewallLock.ExitWriteLock();
|
||||
return;
|
||||
}
|
||||
|
||||
List<IFirewallEntry> expired = null;
|
||||
foreach (var (entry, expiresAt) in _expiring)
|
||||
{
|
||||
if (expiresAt - nowTicks <= 0)
|
||||
{
|
||||
(expired ??= []).Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (expired == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var entry in expired)
|
||||
{
|
||||
_entries.Remove(entry);
|
||||
_expiring.Remove(entry);
|
||||
}
|
||||
|
||||
_indexDirty = true;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IFirewallEntry ToFirewallEntry(object entry) =>
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
if (entry.IndexOf('/') > -1)
|
||||
{
|
||||
return new CidrFirewallEntry(entry);
|
||||
}
|
||||
|
||||
return new SingleIpFirewallEntry(entry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalValidationEntry : BaseFirewallEntry
|
||||
public static void Configure()
|
||||
{
|
||||
private UInt128 _address;
|
||||
|
||||
public IPAddress Address
|
||||
if (_configured)
|
||||
{
|
||||
set => _address = value.ToUInt128();
|
||||
return;
|
||||
}
|
||||
_configured = true;
|
||||
|
||||
var path = Path.Join(Core.BaseDirectory, _path);
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
LoadFrom(JsonConfig.Deserialize<FirewallSettings>(path));
|
||||
}
|
||||
else
|
||||
{
|
||||
var legacyPath = ResolveLegacyCfgPath();
|
||||
if (legacyPath != null)
|
||||
{
|
||||
MigrateLegacyCfg(legacyPath);
|
||||
Save(); // materialize firewall.json; the .cfg is no longer read after this
|
||||
TryMarkLegacyCfgMigrated(legacyPath);
|
||||
}
|
||||
}
|
||||
|
||||
public override UInt128 MinIpAddress => _address;
|
||||
public override UInt128 MaxIpAddress => _address;
|
||||
// Main-thread maintenance: expire TTLs and flush pending writes. No background thread.
|
||||
Timer.DelayCall(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30), Maintenance);
|
||||
}
|
||||
|
||||
public InternalValidationEntry(IPAddress ipAddress) => Address = ipAddress;
|
||||
private static void Maintenance()
|
||||
{
|
||||
ExpireEntries(Core.TickCount);
|
||||
|
||||
if (_dirty)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void MarkDirty() => _dirty = true;
|
||||
|
||||
internal static void LoadFrom(FirewallSettings settings)
|
||||
{
|
||||
if (settings?.Entries == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var record in settings.Entries)
|
||||
{
|
||||
var entry = ToFirewallEntry(record.Value);
|
||||
if (entry == null)
|
||||
{
|
||||
logger.Warning("Ignoring unparseable firewall entry \"{Entry}\"", record.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
var ttl = TimeSpan.Zero;
|
||||
if (record.Expires is { } expires)
|
||||
{
|
||||
ttl = expires - now;
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
{
|
||||
continue; // already expired
|
||||
}
|
||||
}
|
||||
|
||||
Add(entry, ttl, persist: false);
|
||||
}
|
||||
}
|
||||
|
||||
internal static FirewallSettings ToSettings()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nowTicks = Core.TickCount;
|
||||
var list = new List<FirewallEntryRecord>(_entries.Count);
|
||||
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
DateTime? expires = null;
|
||||
if (_expiring.TryGetValue(entry, out var expiresAtTick))
|
||||
{
|
||||
expires = now.AddMilliseconds(expiresAtTick - nowTicks);
|
||||
}
|
||||
|
||||
list.Add(new FirewallEntryRecord { Value = entry.ToString(), Expires = expires });
|
||||
}
|
||||
|
||||
return new FirewallSettings { Entries = list.ToArray() };
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
_dirty = false;
|
||||
var path = Path.Join(Core.BaseDirectory, _path);
|
||||
var tmp = path + ".tmp";
|
||||
JsonConfig.Serialize(tmp, ToSettings());
|
||||
File.Move(tmp, path, overwrite: true); // atomic swap
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locates the legacy firewall.cfg to migrate. The modern convention is <see cref="Core.BaseDirectory"/>,
|
||||
/// checked first; the pre-collapse <c>AdminFirewall</c> used a bare relative path (resolved against the
|
||||
/// process's current working directory), which may differ from <see cref="Core.BaseDirectory"/> when the
|
||||
/// shard is launched from elsewhere, so that's checked as a fallback. Returns null if neither exists.
|
||||
/// </summary>
|
||||
private static string ResolveLegacyCfgPath()
|
||||
{
|
||||
var underBaseDirectory = Path.Join(Core.BaseDirectory, _legacyPath);
|
||||
if (File.Exists(underBaseDirectory))
|
||||
{
|
||||
return underBaseDirectory;
|
||||
}
|
||||
|
||||
return File.Exists(_legacyPath) ? _legacyPath : null;
|
||||
}
|
||||
|
||||
private static void MigrateLegacyCfg(string legacyPath)
|
||||
{
|
||||
var searchValues = System.Buffers.SearchValues.Create("*Xx?");
|
||||
|
||||
using var reader = new StreamReader(legacyPath);
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
line = line.Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.AsSpan().ContainsAny(searchValues))
|
||||
{
|
||||
logger.Warning("Legacy firewall entry \"{Entry}\" ignored during migration", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = ToFirewallEntry(line);
|
||||
if (entry != null)
|
||||
{
|
||||
Add(entry, TimeSpan.Zero, persist: false);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Information("Migrated {Count} entr(ies) from legacy firewall.cfg to firewall.json", _entries.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames the migrated <c>.cfg</c> to <c>firewall.cfg.migrated</c> so it isn't re-scanned on the next
|
||||
/// boot and operators can see it was already migrated. Best-effort: a locked/read-only file must not
|
||||
/// fail startup, since the migration itself (firewall.json) already succeeded.
|
||||
/// </summary>
|
||||
private static void TryMarkLegacyCfgMigrated(string legacyPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Move(legacyPath, legacyPath + ".migrated", overwrite: true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Warning(e, "Could not rename migrated legacy firewall file \"{Path}\"", legacyPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
_entries.Clear();
|
||||
_expiring.Clear();
|
||||
_index = SortedRangeIndex<UInt128>.Empty;
|
||||
_indexDirty = false;
|
||||
_configured = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
Projects/Server/Network/Firewall/FirewallSettings.cs
Normal file
42
Projects/Server/Network/Firewall/FirewallSettings.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: FirewallSettings.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.Text.Json.Serialization;
|
||||
|
||||
namespace Server.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted local firewall entries (manual admin bans). Stored at <c>Configuration/firewall.json</c>.
|
||||
/// Auto-detected rate-limit trips are never persisted — they are contributed to CrowdSec, not stored here.
|
||||
/// </summary>
|
||||
public record FirewallSettings
|
||||
{
|
||||
[JsonPropertyName("entries")]
|
||||
public FirewallEntryRecord[] Entries { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One persisted entry. <see cref="Value"/> is a single IP, a <c>min-max</c> range, or CIDR.
|
||||
/// <see cref="Expires"/> is UTC wall-clock; null means permanent.
|
||||
/// </summary>
|
||||
public record FirewallEntryRecord
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonPropertyName("expires")]
|
||||
public DateTime? Expires { get; set; }
|
||||
}
|
||||
190
Projects/Server/Utilities/IPAddressUtility.cs
Normal file
190
Projects/Server/Utilities/IPAddressUtility.cs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: IPAddressUtility.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.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Server;
|
||||
|
||||
/// <summary>
|
||||
/// Low-level IPAddress conversion and parsing helpers shared by the firewall, ban channel, and
|
||||
/// blocklist. All members are allocation-free (stack buffers only) so they are safe on hot accept
|
||||
/// paths and inside tight parse loops.
|
||||
/// </summary>
|
||||
public static class IPAddressUtility
|
||||
{
|
||||
// Converts an IPAddress to a UInt128 in IPv6 format
|
||||
public static UInt128 ToUInt128(this IPAddress ip)
|
||||
{
|
||||
if (ip.AddressFamily == AddressFamily.InterNetwork && !ip.IsIPv4MappedToIPv6)
|
||||
{
|
||||
Span<byte> integer = stackalloc byte[4];
|
||||
return !ip.TryWriteBytes(integer, out _)
|
||||
? (UInt128)0
|
||||
: new UInt128(0, 0xFFFF00000000UL | BinaryPrimitives.ReadUInt32BigEndian(integer));
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
if (!ip.TryWriteBytes(bytes, out _))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]);
|
||||
var low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8));
|
||||
|
||||
return new UInt128(high, low);
|
||||
}
|
||||
|
||||
// Converts a UInt128 in IPv6 format to an IPAddress
|
||||
public static IPAddress ToIpAddress(this UInt128 value, bool mapToIpv6 = false)
|
||||
{
|
||||
// IPv4 mapped IPv6 address
|
||||
if (!mapToIpv6 && value >= 0xFFFF00000000UL && value <= 0xFFFFFFFFFFFFUL)
|
||||
{
|
||||
var newAddress = IPAddress.HostToNetworkOrder((int)value);
|
||||
return new IPAddress(unchecked((uint)newAddress));
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[16]; // 128 bits for IPv6 address
|
||||
((IBinaryInteger<UInt128>)value).WriteBigEndian(bytes);
|
||||
|
||||
return new IPAddress(bytes);
|
||||
}
|
||||
|
||||
/// <summary>Extracts the big-endian uint of an <see cref="AddressFamily.InterNetwork"/> address.</summary>
|
||||
public static bool TryV4(IPAddress ip, out uint v)
|
||||
{
|
||||
Span<byte> b = stackalloc byte[4];
|
||||
if (ip.TryWriteBytes(b, out var n) && n == 4)
|
||||
{
|
||||
v = ((uint)b[0] << 24) | ((uint)b[1] << 16) | ((uint)b[2] << 8) | b[3];
|
||||
return true;
|
||||
}
|
||||
|
||||
v = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the embedded v4 uint from a v4-mapped-v6 address directly from the mapped bytes,
|
||||
/// avoiding the allocation of <see cref="IPAddress.MapToIPv4"/>.
|
||||
/// </summary>
|
||||
public static bool TryMappedV4(IPAddress ip, out uint v)
|
||||
{
|
||||
Span<byte> b = stackalloc byte[16];
|
||||
if (ip.TryWriteBytes(b, out var n) && n == 16)
|
||||
{
|
||||
v = ((uint)b[12] << 24) | ((uint)b[13] << 16) | ((uint)b[14] << 8) | b[15];
|
||||
return true;
|
||||
}
|
||||
|
||||
v = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Parses a dotted-quad IPv4 literal into a big-endian uint. Allocation-free, strict.</summary>
|
||||
public static bool TryParseV4(ReadOnlySpan<char> s, out uint v)
|
||||
{
|
||||
v = 0;
|
||||
uint acc = 0;
|
||||
int octet = 0, digits = 0, dots = 0;
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (c == '.')
|
||||
{
|
||||
if (digits == 0 || octet > 255)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
acc = (acc << 8) | (uint)octet;
|
||||
dots++;
|
||||
octet = 0;
|
||||
digits = 0;
|
||||
}
|
||||
else if (c is >= '0' and <= '9')
|
||||
{
|
||||
octet = octet * 10 + (c - '0');
|
||||
if (++digits > 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (dots != 3 || digits == 0 || octet > 255)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
v = (acc << 8) | (uint)octet;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UTF-8/ASCII byte overload of <see cref="TryParseV4(ReadOnlySpan{char}, out uint)"/>, mirroring its
|
||||
/// validation exactly so the blocklist can parse dotted-quads straight from file bytes with no
|
||||
/// per-line string allocation.
|
||||
/// </summary>
|
||||
public static bool TryParseV4(ReadOnlySpan<byte> s, out uint v)
|
||||
{
|
||||
v = 0;
|
||||
uint acc = 0;
|
||||
int octet = 0, digits = 0, dots = 0;
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (c == (byte)'.')
|
||||
{
|
||||
if (digits == 0 || octet > 255)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
acc = (acc << 8) | (uint)octet;
|
||||
dots++;
|
||||
octet = 0;
|
||||
digits = 0;
|
||||
}
|
||||
else if (c is >= (byte)'0' and <= (byte)'9')
|
||||
{
|
||||
octet = octet * 10 + (c - '0');
|
||||
if (++digits > 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (dots != 3 || digits == 0 || octet > 255)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
v = (acc << 8) | (uint)octet;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,45 +108,6 @@ public static partial class Utility
|
|||
}
|
||||
}
|
||||
|
||||
// Converts an IPAddress to a UInt128 in IPv6 format
|
||||
public static UInt128 ToUInt128(this IPAddress ip)
|
||||
{
|
||||
if (ip.AddressFamily == AddressFamily.InterNetwork && !ip.IsIPv4MappedToIPv6)
|
||||
{
|
||||
Span<byte> integer = stackalloc byte[4];
|
||||
return !ip.TryWriteBytes(integer, out _)
|
||||
? (UInt128)0
|
||||
: new UInt128(0, 0xFFFF00000000UL | BinaryPrimitives.ReadUInt32BigEndian(integer));
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
if (!ip.TryWriteBytes(bytes, out _))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var high = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]);
|
||||
var low = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8));
|
||||
|
||||
return new UInt128(high, low);
|
||||
}
|
||||
|
||||
// Converts a UInt128 in IPv6 format to an IPAddress
|
||||
public static IPAddress ToIpAddress(this UInt128 value, bool mapToIpv6 = false)
|
||||
{
|
||||
// IPv4 mapped IPv6 address
|
||||
if (!mapToIpv6 && value >= 0xFFFF00000000UL && value <= 0xFFFFFFFFFFFFUL)
|
||||
{
|
||||
var newAddress = IPAddress.HostToNetworkOrder((int)value);
|
||||
return new IPAddress(unchecked((uint)newAddress));
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[16]; // 128 bits for IPv6 address
|
||||
((IBinaryInteger<UInt128>)value).WriteBigEndian(bytes);
|
||||
|
||||
return new IPAddress(bytes);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 CreateCidrAddress(ReadOnlySpan<byte> bytes, int prefixLength, bool isMax)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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 bool Remove(object obj, bool save = true)
|
||||
{
|
||||
var entry = ToFirewallEntry(obj);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Firewall.Remove(entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (save)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Add(object obj) => Add(ToFirewallEntry(obj));
|
||||
|
||||
public static bool Add(IFirewallEntry entry, bool save = true)
|
||||
{
|
||||
if (!Firewall.Add(entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (save)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
Firewall.ReadFirewallSet(firewallSet =>
|
||||
{
|
||||
using var op = new StreamWriter(firewallConfigPath);
|
||||
foreach (var entry in firewallSet)
|
||||
{
|
||||
op.WriteLine(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue