using System.Globalization; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using MoShiro.Protocol; namespace MoShiro.Measurement; /// /// 本机的公网地址:Agent 注册之后在 hello 里上报,面板用它自动填 public_host 和 ipv6_host,不需要管理员手填。 /// 只列出「别人能拨进来」的地址:IPv4 要排除私网、CGNAT、回环、链路本地;IPv6 只要全球单播(2000::/3), /// 并且排除临时地址、tentative、deprecated 和文档/6to4/Teredo 段。读不到网卡信息时返回空,面板退回到管理员手填或观察到的来源地址。 /// public static class LocalAddresses { public static IReadOnlyList Sample() { var result = new List(); HashSet unusable = OperatingSystem.IsLinux() ? UnusableIpv6(Read("/proc/net/if_inet6")) : []; try { foreach (NetworkInterface adapter in NetworkInterface.GetAllNetworkInterfaces()) { if (adapter.OperationalStatus != OperationalStatus.Up || adapter.NetworkInterfaceType is NetworkInterfaceType.Loopback or NetworkInterfaceType.Tunnel) { continue; } foreach (UnicastIPAddressInformation unicast in adapter.GetIPProperties().UnicastAddresses) { IPAddress address = unicast.Address; bool usable = address.AddressFamily == AddressFamily.InterNetwork ? PublicAddress.IsPublicIpv4(address) : PublicAddress.IsGlobalIpv6(address) && !unusable.Contains(address.ToString()) && !IsTemporary(unicast); if (usable && !result.Contains(address.ToString())) { result.Add(address.ToString()); } } } } catch (Exception ex) when (ex is NetworkInformationException or PlatformNotSupportedException) { return []; } // 稳定顺序:同一台机器每次上报的顺序一样,面板不会因为顺序变化而改地址 result.Sort(StringComparer.Ordinal); return result; } /// Linux 的 /proc/net/if_inet6:临时(0x01)、dadfailed(0x08)、deprecated(0x20)、tentative(0x40)的地址不能当作对外地址。 public static HashSet UnusableIpv6(string? procNetIfInet6) { var result = new HashSet(StringComparer.Ordinal); if (procNetIfInet6 is null) { return result; } foreach (string line in procNetIfInet6.Split('\n')) { string[] fields = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (fields.Length != 6 || fields[0].Length != 32 || !uint.TryParse(fields[4], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint flags) || (flags & 0x69) == 0) { continue; } var bytes = new byte[16]; for (int i = 0; i < 16; i++) { bytes[i] = byte.Parse(fields[0].AsSpan(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); } result.Add(new IPAddress(bytes).ToString()); } return result; } // Windows 和 macOS:随机后缀就是隐私(临时)地址 private static bool IsTemporary(UnicastIPAddressInformation unicast) { try { return OperatingSystem.IsWindows() && (unicast.SuffixOrigin == SuffixOrigin.Random || unicast.DuplicateAddressDetectionState is DuplicateAddressDetectionState.Tentative or DuplicateAddressDetectionState.Duplicate); } catch (PlatformNotSupportedException) { return false; } } 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; } } }