/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: BlocklistSnapshot.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.Buffers.Text;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Server.Collections;
namespace Server.Network.Bans;
///
/// Immutable dual-stack blocklist. Singles and CIDRs are folded into a single sorted, coalesced
/// interval index per family: IPv4 as ranges (lean for the millions-strong common
/// case), IPv6 as ranges (empty unless the feed carries v6). Immutable → lock-free reads.
///
public sealed class BlocklistSnapshot
{
public static readonly BlocklistSnapshot Empty = new(SortedRangeIndex.Empty, SortedRangeIndex.Empty);
private readonly SortedRangeIndex _v4;
private readonly SortedRangeIndex _v6;
public int Count => _v4.Count + _v6.Count;
private BlocklistSnapshot(SortedRangeIndex v4, SortedRangeIndex v6)
{
_v4 = v4;
_v6 = v6;
}
///
/// Parses a blocklist directly from its UTF-8/ASCII file bytes — one line at a time, splitting on
/// '\n' with no per-line string allocation. IPv4 singles and CIDRs are parsed straight from the
/// byte span; IPv6 (the rare path) decodes the single address token and defers to the framework parser.
/// Malformed lines increment and never throw. Build-time intermediates use
/// the multithreaded pool because this runs off the game loop on the reload/bootstrap thread.
///
public static BlocklistSnapshot Build(ReadOnlySpan data, out int parsed, out int skipped)
{
parsed = 0;
skipped = 0;
// Only the two final index arrays (allocated inside SortedRangeIndex.Build) hit the heap; every
// build-time buffer here is a pooled ref list. mt: true is required — this runs off the game loop.
using var v4 = PooledRefList.Range>.Create(mt: true);
using var v6 = PooledRefList.Range>.Create(mt: true);
var rest = data;
while (!rest.IsEmpty)
{
ReadOnlySpan line;
var nl = rest.IndexOf((byte)'\n');
if (nl >= 0)
{
line = rest[..nl];
rest = rest[(nl + 1)..];
}
else
{
line = rest;
rest = default;
}
line = line[Ascii.Trim(line)];
if (line.IsEmpty || line[0] == (byte)'#' || line[0] == (byte)';')
{
continue;
}
var slash = line.IndexOf((byte)'/');
var addr = slash >= 0 ? line[..slash] : line;
var bitsToken = slash >= 0 ? line[(slash + 1)..] : default;
if (addr.IndexOf((byte)':') < 0)
{
// IPv4 single or CIDR — parsed straight from the byte span.
if (slash >= 0)
{
if (IPAddressUtility.TryParseV4(addr, out var ip) &&
TryParseBits(bitsToken, out var bits) && bits is >= 0 and <= 32)
{
var size = bits == 0 ? 0xFFFFFFFFu : (1u << (32 - bits)) - 1;
var b = ip & ~size;
v4.Add(new SortedRangeIndex.Range(b, b + size));
parsed++;
}
else
{
skipped++;
}
}
else if (IPAddressUtility.TryParseV4(addr, out var ip))
{
v4.Add(new SortedRangeIndex.Range(ip, ip));
parsed++;
}
else
{
skipped++;
}
}
else if (TryDecodeV6(addr, out var v))
{
// IPv6 is rare in these feeds; the single token was decoded and framework-parsed above.
if (slash >= 0)
{
if (TryParseBits(bitsToken, out var bits) && bits is >= 0 and <= 128)
{
var mask = bits == 0 ? UInt128.Zero : ~((UInt128.One << (128 - bits)) - 1);
var b = v & mask;
v6.Add(new SortedRangeIndex.Range(b, b | ~mask));
parsed++;
}
else
{
skipped++;
}
}
else
{
v6.Add(new SortedRangeIndex.Range(v, v));
parsed++;
}
}
else
{
skipped++;
}
}
v4.Sort(SortedRangeIndex.ByMin);
v6.Sort(SortedRangeIndex.ByMin);
return new BlocklistSnapshot(SortedRangeIndex.Build(v4.AsSpan()), SortedRangeIndex.Build(v6.AsSpan()));
}
// Decodes a single IPv6 address token from ASCII bytes and validates it via the framework parser.
private static bool TryDecodeV6(ReadOnlySpan addr, out UInt128 v)
{
v = UInt128.Zero;
if (addr.Length > 45)
{
return false;
}
Span chars = stackalloc char[addr.Length];
for (var i = 0; i < addr.Length; i++)
{
chars[i] = (char)addr[i];
}
if (!IPAddress.TryParse(chars, out var a) || a.AddressFamily != AddressFamily.InterNetworkV6)
{
return false;
}
v = a.ToUInt128();
return true;
}
private static bool TryParseBits(ReadOnlySpan token, out int bits)
{
if (Utf8Parser.TryParse(token, out bits, out var consumed) && consumed == token.Length)
{
return true;
}
bits = 0;
return false;
}
///
/// Plain set membership, for callers whose set is an ALLOWlist (see ) and for
/// whom would read backwards. The interval machinery is direction-agnostic.
///
public bool Contains(IPAddress ip) => IsBanned(ip);
public bool IsBanned(IPAddress ip)
{
if (ip.IsIPv4MappedToIPv6)
{
// v6-encoded v4 must not dodge the v4 set; extract the embedded v4 uint directly.
return IPAddressUtility.TryMappedV4(ip, out var mv) && _v4.Contains(mv);
}
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return IPAddressUtility.TryV4(ip, out var v) && _v4.Contains(v);
}
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
{
return _v6.Contains(ip.ToUInt128());
}
return false;
}
}