/*************************************************************************
* 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 . *
*************************************************************************/
using System;
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
namespace Server;
///
/// 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.
///
public static class IPAddressUtility
{
// Converts an IPAddress to a UInt128 in IPv6 format.
// The IsIPv4MappedToIPv6 clause below looks redundant (the BCL only ever sets it on InterNetworkV6),
// but it guards the v4 -> UInt128 -> IPAddress round-trip, which can return a mapped v6 address for
// what is really a v4 one.
//TODO Rework as an explicit "to canonical v6 bits" step that needs no family check
// (see dev-docs/networking-packets.md, "IP Address Normalization")
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;
}
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 bytes = stackalloc byte[16]; // 128 bits for IPv6 address
((IBinaryInteger)value).WriteBigEndian(bytes);
return new IPAddress(bytes);
}
///
/// Parses a.b.c.d/n, ::/n, or a bare address (treated as a single-host range) into an
/// inclusive range in normalized IPv6 form. A bare IPv4 prefix is widened by 96
/// bits so v4 and v6 ranges are directly comparable. Returns false on anything malformed.
///
public static bool TryParseCidrRange(ReadOnlySpan cidr, out UInt128 min, out UInt128 max)
{
min = default;
max = default;
var slash = cidr.IndexOf('/');
if (!IPAddress.TryParse(slash >= 0 ? cidr[..slash] : cidr, out var ip))
{
return false;
}
var isV6 = ip.AddressFamily == AddressFamily.InterNetworkV6;
var maxPrefixLength = isV6 ? 128 : 32;
int prefixLength;
if (slash < 0)
{
prefixLength = maxPrefixLength;
}
else if (!int.TryParse(cidr[(slash + 1)..], out prefixLength) ||
prefixLength < 0 || prefixLength > maxPrefixLength)
{
return false;
}
if (!isV6)
{
prefixLength += 96; // 32 -> 128
}
Span bytes = stackalloc byte[16];
ip.WriteMappedIPv6To(bytes);
min = Utility.CreateCidrAddress(bytes, prefixLength, false);
max = Utility.CreateCidrAddress(bytes, prefixLength, true);
return true;
}
/// Extracts the big-endian uint of an address.
public static bool TryV4(IPAddress ip, out uint v)
{
Span 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;
}
///
/// Extracts the embedded v4 uint from a v4-mapped-v6 address directly from the mapped bytes,
/// avoiding the allocation of .
///
public static bool TryMappedV4(IPAddress ip, out uint v)
{
Span 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;
}
/// Parses a dotted-quad IPv4 literal into a big-endian uint. Allocation-free, strict.
public static bool TryParseV4(ReadOnlySpan s, out uint v)
{
v = 0;
uint acc = 0;
int octet = 0, digits = 0, dots = 0;
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
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;
}
///
/// UTF-8/ASCII byte overload of , mirroring its
/// validation exactly so the blocklist can parse dotted-quads straight from file bytes with no
/// per-line string allocation.
///
public static bool TryParseV4(ReadOnlySpan s, out uint v)
{
v = 0;
uint acc = 0;
int octet = 0, digits = 0, dots = 0;
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
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;
}
}