107 lines
4.2 KiB
C#
107 lines
4.2 KiB
C#
using System.Globalization;
|
||
using System.Net;
|
||
using System.Net.NetworkInformation;
|
||
using System.Net.Sockets;
|
||
using MoShiro.Protocol;
|
||
|
||
namespace MoShiro.Measurement;
|
||
|
||
/// <summary>
|
||
/// 本机的公网地址:Agent 注册之后在 hello 里上报,面板用它自动填 <c>public_host</c> 和 <c>ipv6_host</c>,不需要管理员手填。
|
||
/// 只列出「别人能拨进来」的地址:IPv4 要排除私网、CGNAT、回环、链路本地;IPv6 只要全球单播(2000::/3),
|
||
/// 并且排除临时地址、tentative、deprecated 和文档/6to4/Teredo 段。读不到网卡信息时返回空,面板退回到管理员手填或观察到的来源地址。
|
||
/// </summary>
|
||
public static class LocalAddresses
|
||
{
|
||
public static IReadOnlyList<string> Sample()
|
||
{
|
||
var result = new List<string>();
|
||
HashSet<string> 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;
|
||
}
|
||
|
||
/// <summary>Linux 的 <c>/proc/net/if_inet6</c>:临时(0x01)、dadfailed(0x08)、deprecated(0x20)、tentative(0x40)的地址不能当作对外地址。</summary>
|
||
public static HashSet<string> UnusableIpv6(string? procNetIfInet6)
|
||
{
|
||
var result = new HashSet<string>(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;
|
||
}
|
||
}
|
||
}
|