using System.Globalization; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using MoShiro.Protocol; namespace MoShiro.Measurement; /// 诊断采集进程所在的网络环境。宿主网络容器看到的是主机;桥接容器看到自己的网络环境。 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; } } }