using System.Net;
using System.Net.Sockets;
namespace Sunfish.Net;
/// <summary>
/// Listas de direcciones como AllowFrom: IP sueltas ("127.0.0.1", "::1"), redes en notacion CIDR
/// ("192.168.1.0/24", "fd00::/8") o "*" (cualquiera). Las IPv4 que llegan como IPv6
/// (::ffff:a.b.c.d) se comparan como IPv4.
/// </summary>
public static class AddressRules
{
public static bool IsValid(string rule) => rule == "*" || TryParse(rule, out _, out _);
public static bool Matches(IEnumerable<string> rules, IPAddress? address)
{
if (address is null)
return false;
if (address.IsIPv4MappedToIPv6)
address = address.MapToIPv4();
foreach (var rule in rules)
{
if (rule == "*")
return true;
if (TryParse(rule, out var network, out int prefix) && InNetwork(address, network, prefix))
return true;
}
return false;
}
private static bool TryParse(string rule, out IPAddress network, out int prefix)
{
network = IPAddress.None;
prefix = 0;
string text = rule.Trim();
int slash = text.IndexOf('/');
if (!IPAddress.TryParse(slash < 0 ? text : text[..slash], out var parsed))
return false;
network = parsed.IsIPv4MappedToIPv6 ? parsed.MapToIPv4() : parsed;
int max = network.AddressFamily == AddressFamily.InterNetwork ? 32 : 128;
if (slash < 0)
{
prefix = max;
return true;
}
return int.TryParse(text[(slash + 1)..], out prefix) && prefix >= 0 && prefix <= max;
}
private static bool InNetwork(IPAddress address, IPAddress network, int prefix)
{
if (address.AddressFamily != network.AddressFamily)
return false;
var a = address.GetAddressBytes();
var n = network.GetAddressBytes();
int full = prefix / 8, rest = prefix % 8;
for (int i = 0; i < full; i++)
if (a[i] != n[i])
return false;
if (rest == 0)
return true;
int mask = 0xFF << (8 - rest) & 0xFF;
return (a[full] & mask) == (n[full] & mask);
}
}