Files
MoShiro/backend/shared/MoShiro.Measurement/Platform/Ipv6Diagnostics.cs
T
justme 84f9d211bc
ci/woodpecker/pr/verify Pipeline failed
ci/woodpecker/pr/web-build unknown status
backend: require confirmation for signed channel updates [CI]
2026-10-07 12:57:20 +08:00

155 lines
5.8 KiB
C#

using System.Globalization;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using MoShiro.Protocol;
namespace MoShiro.Measurement;
/// <summary>诊断采集进程所在的网络环境。宿主网络容器看到的是主机;桥接容器看到自己的网络环境。</summary>
public static class Ipv6Diagnostics
{
public static Ipv6Diagnostic Sample(bool? listening = null, string? networkScope = null, string? hostProcRoot = null)
{
bool supported = Socket.OSSupportsIPv6;
bool? address = null;
bool? route = null;
if (supported)
{
if (OperatingSystem.IsLinux())
{
address = ParseAddresses(Read("/proc/net/if_inet6"));
route = ParseRoutes(Read("/proc/net/ipv6_route"));
}
else if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
{
try
{
address = false;
route = false;
foreach (NetworkInterface adapter in NetworkInterface.GetAllNetworkInterfaces())
{
if (adapter.OperationalStatus != OperationalStatus.Up || adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback)
{
continue;
}
IPInterfaceProperties properties = adapter.GetIPProperties();
foreach (UnicastIPAddressInformation unicast in properties.UnicastAddresses)
{
if (IsGlobal(unicast.Address))
{
address = true;
}
}
foreach (GatewayIPAddressInformation gateway in properties.GatewayAddresses)
{
if (gateway.Address.AddressFamily == AddressFamily.InterNetworkV6 && !gateway.Address.Equals(IPAddress.IPv6Any))
{
route = true;
}
}
}
}
catch (Exception ex) when (ex is NetworkInformationException or PlatformNotSupportedException)
{
address = null;
route = null;
}
}
}
string scope = networkScope is "host" or "container" ? networkScope
: File.Exists("/run/.containerenv") || File.Exists("/.dockerenv") ? "container" : "host";
bool? hostAddress = scope == "host" ? address : null;
bool? hostRoute = scope == "host" ? route : null;
if (scope == "container" && OperatingSystem.IsLinux() && hostProcRoot is not null)
{
// /host/proc/net 通常指向当前进程的网络空间;宿主 PID 1 才是宿主网络。
hostAddress = ParseAddresses(Read(Path.Combine(hostProcRoot, "1/net/if_inet6")));
hostRoute = ParseRoutes(Read(Path.Combine(hostProcRoot, "1/net/ipv6_route")));
}
Ipv6Diagnostic result = Evaluate(supported, listening, address, route);
result.NetworkScope = scope;
result.HostHasGlobalAddress = hostAddress;
result.HostHasDefaultRoute = hostRoute;
if (scope == "container" && address == false && hostAddress == true)
{
result.State = "container_network_not_ready";
}
return result;
}
public static Ipv6Diagnostic Evaluate(bool supported, bool? listening, bool? address, bool? route) => new()
{
SocketSupported = supported,
Listening = listening,
HasGlobalAddress = address,
HasDefaultRoute = route,
State = !supported ? "unsupported" : address == false ? "no_global_address" : listening == false ? "not_listening"
: route == false ? "no_default_route" : address is null || route is null ? "unknown" : "ready",
};
public static bool? ParseAddresses(string? text)
{
if (text is null)
{
return null;
}
foreach (string line in text.Split('\n'))
{
string[] fields = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (fields.Length != 6 || fields[0].Length != 32 || fields[0][0] is not ('2' or '3') || fields[3] != "00")
{
continue;
}
// tentative、DAD failed 和 deprecated 地址不能作为就绪的公网地址。
if (uint.TryParse(fields[4], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint flags) && (flags & 0x68) == 0)
{
return true;
}
}
return false;
}
public static bool? ParseRoutes(string? text)
{
if (text is null)
{
return null;
}
foreach (string line in text.Split('\n'))
{
string[] fields = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (fields.Length == 10 && fields[0] == "00000000000000000000000000000000" && fields[1] == "00" && fields[9] != "lo"
&& uint.TryParse(fields[8], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint flags) && (flags & 1) != 0 && (flags & 0x200) == 0)
{
return true;
}
}
return false;
}
private static bool IsGlobal(IPAddress address) => address.AddressFamily == AddressFamily.InterNetworkV6 && (address.GetAddressBytes()[0] & 0xE0) == 0x20;
private static string? Read(string path)
{
try
{
return File.Exists(path) ? File.ReadAllText(path) : null;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return null;
}
}
}