155 lines
5.8 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|