backend: require confirmation for signed channel updates [CI]
This commit is contained in:
1 parent
7160de2a36
commit
84f9d211bc
62 files changed
+1717
-70
No files matched your search
@@ -0,0 +1,154 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MoShiro.Protocol;
|
||||
|
||||
/// <summary>当前网络环境的 IPv6 诊断。仅上报状态,不传地址、接口名称或路由内容。</summary>
|
||||
public sealed class Ipv6Diagnostic
|
||||
{
|
||||
[JsonPropertyName("socket_supported")]
|
||||
public bool SocketSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("listening")]
|
||||
public bool? Listening { get; set; }
|
||||
|
||||
[JsonPropertyName("network_scope")]
|
||||
public string NetworkScope { get; set; } = "unknown";
|
||||
|
||||
[JsonPropertyName("host_has_global_address")]
|
||||
public bool? HostHasGlobalAddress { get; set; }
|
||||
|
||||
[JsonPropertyName("host_has_default_route")]
|
||||
public bool? HostHasDefaultRoute { get; set; }
|
||||
|
||||
[JsonPropertyName("has_global_address")]
|
||||
public bool? HasGlobalAddress { get; set; }
|
||||
|
||||
[JsonPropertyName("has_default_route")]
|
||||
public bool? HasDefaultRoute { get; set; }
|
||||
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; } = "unknown";
|
||||
}
|
||||
@@ -281,6 +281,9 @@ public sealed class UpdateStatus
|
||||
/// <summary>Probe 的 <c>fleet.heartbeat</c> 附加字段:更新器的状态。</summary>
|
||||
public sealed class ProbeHeartbeatExtra
|
||||
{
|
||||
[JsonPropertyName("ipv6")]
|
||||
public Ipv6Diagnostic? Ipv6 { get; set; }
|
||||
|
||||
[JsonPropertyName("update")]
|
||||
public UpdateStatus? Update { get; set; }
|
||||
}
|
||||
@@ -288,6 +291,9 @@ public sealed class ProbeHeartbeatExtra
|
||||
/// <summary>Probe 的 <c>fleet.hello</c> 附加字段:系统、架构和构建信息(只用于展示和更新)。</summary>
|
||||
public sealed class ProbeHelloMeta
|
||||
{
|
||||
[JsonPropertyName("ipv6")]
|
||||
public Ipv6Diagnostic? Ipv6 { get; set; }
|
||||
|
||||
[JsonPropertyName("platform")]
|
||||
public PlatformInfo? Platform { get; set; }
|
||||
}
|
||||
@@ -295,6 +301,9 @@ public sealed class ProbeHelloMeta
|
||||
/// <summary><c>fleet.hello</c> 的附加字段。</summary>
|
||||
public sealed class AgentHelloMeta
|
||||
{
|
||||
[JsonPropertyName("ipv6")]
|
||||
public Ipv6Diagnostic? Ipv6 { get; set; }
|
||||
|
||||
/// <summary>本机是否启用了 IPv6 监听。旧 Agent 缺省为 false,面板不会给它安排 IPv6 会话。</summary>
|
||||
[JsonPropertyName("ipv6_supported")]
|
||||
public bool Ipv6Supported { get; set; }
|
||||
@@ -383,6 +392,9 @@ public sealed class ConnectedProbe
|
||||
/// <summary><c>fleet.heartbeat</c> 的附加字段:只有进程内的计数,没有主机信息。</summary>
|
||||
public sealed class AgentHeartbeatExtra
|
||||
{
|
||||
[JsonPropertyName("ipv6")]
|
||||
public Ipv6Diagnostic? Ipv6 { get; set; }
|
||||
|
||||
/// <summary>当前连着的 Probe 会话数(等于 <see cref="Probes"/> 的个数)。</summary>
|
||||
[JsonPropertyName("sessions")]
|
||||
public int Sessions { get; set; }
|
||||
@@ -554,6 +566,7 @@ public static class FragmentKeys
|
||||
[JsonSerializable(typeof(AgentEnrollMeta))]
|
||||
[JsonSerializable(typeof(PlatformInfo))]
|
||||
[JsonSerializable(typeof(AgentHelloMeta))]
|
||||
[JsonSerializable(typeof(Ipv6Diagnostic))]
|
||||
[JsonSerializable(typeof(AgentHeartbeatExtra))]
|
||||
[JsonSerializable(typeof(RawChunkHeader))]
|
||||
public sealed partial class MoShiroJsonContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,50 @@
|
||||
using MoShiro.Measurement;
|
||||
|
||||
namespace MoShiro.Measurement.Tests;
|
||||
|
||||
public sealed class Ipv6DiagnosticsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Addresses_IgnoresLocalTentativeFailedAndDeprecatedAddresses()
|
||||
{
|
||||
Assert.Null(Ipv6Diagnostics.ParseAddresses(null));
|
||||
Assert.False(Ipv6Diagnostics.ParseAddresses(""));
|
||||
Assert.False(Ipv6Diagnostics.ParseAddresses("00000000000000000000000000000001 01 80 10 80 lo\nfe800000000000000000000000000001 02 40 20 80 eth0"));
|
||||
foreach (string flags in new[] { "40", "08", "20" })
|
||||
{
|
||||
Assert.False(Ipv6Diagnostics.ParseAddresses($"20010db8000000000000000000000001 02 40 00 {flags} eth0"));
|
||||
}
|
||||
|
||||
Assert.True(Ipv6Diagnostics.ParseAddresses("20010db8000000000000000000000001 02 40 00 80 eth0"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Routes_RequiresAnActiveNonLoopbackDefaultRoute()
|
||||
{
|
||||
Assert.Null(Ipv6Diagnostics.ParseRoutes(null));
|
||||
Assert.False(Ipv6Diagnostics.ParseRoutes(""));
|
||||
string prefix = "00000000000000000000000000000000 00 00000000000000000000000000000000 00 fe800000000000000000000000000001 00000400 00000000 00000000 ";
|
||||
Assert.True(Ipv6Diagnostics.ParseRoutes(prefix + "00000003 eth0"));
|
||||
Assert.False(Ipv6Diagnostics.ParseRoutes(prefix + "00000201 eth0"));
|
||||
Assert.False(Ipv6Diagnostics.ParseRoutes(prefix + "00000001 lo"));
|
||||
Assert.False(Ipv6Diagnostics.ParseRoutes(prefix + "00000000 eth0"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, true, true, true, "unsupported")]
|
||||
[InlineData(true, true, false, true, "no_global_address")]
|
||||
[InlineData(true, false, true, true, "not_listening")]
|
||||
[InlineData(true, true, true, false, "no_default_route")]
|
||||
[InlineData(true, true, true, true, "ready")]
|
||||
public void Evaluate_DistinguishesConfigurationFromAddressAndRouting(bool supported, bool listening, bool address, bool route, string expected)
|
||||
{
|
||||
Assert.Equal(expected, Ipv6Diagnostics.Evaluate(supported, listening, address, route).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Probe_DoesNotRequireAnInboundListener_AndUnavailableDataStaysUnknown()
|
||||
{
|
||||
Assert.Equal("ready", Ipv6Diagnostics.Evaluate(true, null, true, true).State);
|
||||
Assert.Equal("unknown", Ipv6Diagnostics.Evaluate(true, null, null, null).State);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user