agent: unify listening and dialing with directed mutual measurements

Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
justme committed 2026-10-21 11:55:36 +08:00
1 parent d5ebf29e57
commit ad5efc62ff
206 files changed
+4202 -4964

No files matched your search

@@ -2,23 +2,12 @@ using Fleet.Rpc;
namespace MoShiro.Protocol;
/// <summary>节点的角色。服务端按节点 ID 的前缀判断;声明表用它限定哪些角色可以参与某个方法或通道。</summary>
public static class NodeRoles
{
public const string Agent = "agent";
public const string Probe = "probe";
public static string? Of(string nodeId) =>
nodeId.StartsWith(AgentIds.Prefix, StringComparison.Ordinal) ? Agent
: nodeId.StartsWith(ProbeId.Prefix, StringComparison.Ordinal) ? Probe
: null;
}
/// <summary>
/// 所有方法和二进制通道的方向声明,单一真源(框架的 <c>fleet.*</c> 见 <see cref="FleetMethodTable"/>)。
/// 新增一个方法:在 <see cref="MoShiroMethods"/> 里加名字和参数,在这里加一行声明,再在对应一侧注册处理函数;
/// 声明了但没人处理、或者处理了但没声明,都会被测试和启动时的检查发现。
/// 同一个方法名只有一个发起方;节点面是服务端和节点(B 和 A、P),对等面是 Probe(拨号方)和 Agent(监听方)。
/// 同一个方法名只有一个发起方;节点面是服务端和节点,对等面是拨号方和监听方。所有节点是同一种角色:
/// 能做什么由节点的配置(是否监听)和 Backend 下发的期望状态决定,声明表不区分节点类型。
/// </summary>
public static class MoShiroMethodTable
{
@@ -27,30 +16,30 @@ public static class MoShiroMethodTable
new MethodTable(
[
// 节点面:Backend → 节点
MethodSpec.Request(MoShiroMethods.TestRun, RpcSide.Server, NodeRoles.Agent),
MethodSpec.Request(MoShiroMethods.TestCancel, RpcSide.Server, NodeRoles.Agent),
MethodSpec.Request(MoShiroMethods.ProbeDisconnect, RpcSide.Server, NodeRoles.Agent),
MethodSpec.Notification(MoShiroMethods.ProbeLinkRetry, RpcSide.Server, NodeRoles.Probe),
MethodSpec.Request(MoShiroMethods.TestRun, RpcSide.Server),
MethodSpec.Request(MoShiroMethods.TestCancel, RpcSide.Server),
MethodSpec.Request(MoShiroMethods.DialerDisconnect, RpcSide.Server),
MethodSpec.Notification(MoShiroMethods.LinkRetry, RpcSide.Server),
MethodSpec.Notification(MoShiroMethods.UpdateOffer, RpcSide.Server),
MethodSpec.Notification(MoShiroMethods.UpdateApply, RpcSide.Server),
MethodSpec.Notification(MoShiroMethods.Schedule, RpcSide.Server),
// 节点面:节点 → Backend
MethodSpec.Notification(MoShiroMethods.TestStatus, RpcSide.Node, NodeRoles.Agent),
MethodSpec.Notification(MoShiroMethods.Event, RpcSide.Node, NodeRoles.Agent),
MethodSpec.Notification(MoShiroMethods.Host, RpcSide.Node), // Agent 和 Probe 都上报主机资源占用的汇总
MethodSpec.Notification(MoShiroMethods.TestStatus, RpcSide.Node),
MethodSpec.Notification(MoShiroMethods.Event, RpcSide.Node),
MethodSpec.Notification(MoShiroMethods.Host, RpcSide.Node),
// 对等面:Probe(拨号方)→ Agent(监听方)
// 对等面:拨号方 → 监听方
MethodSpec.Request(MoShiroMethods.Clock, RpcSide.Dialer),
// 对等面:Agent → Probe
// 对等面:监听方 → 拨号方
MethodSpec.Request(MoShiroMethods.TestPhase, RpcSide.Listener),
MethodSpec.Request(MoShiroMethods.TestPhaseEnd, RpcSide.Listener),
MethodSpec.Notification(MoShiroMethods.TestAbort, RpcSide.Listener),
],
[
// 原始块:只有 Agent 往 Backend 发;列块:Probe 经对等会话发给 Agent
new ChannelSpec(MoShiroChannels.RawChunk, RpcSide.Node, [NodeRoles.Agent]),
// 原始块:节点往 Backend 发;列块:拨号方经对等会话发给监听方
new ChannelSpec(MoShiroChannels.RawChunk, RpcSide.Node),
new ChannelSpec(MoShiroChannels.ColumnBlock, RpcSide.Dialer),
]));
}
+206 -162
View File
@@ -9,8 +9,8 @@ public static class MoShiroMethods
{
public const string TestRun = "moshiro.test.run";
public const string TestCancel = "moshiro.test.cancel";
public const string ProbeDisconnect = "moshiro.probe.disconnect";
public const string ProbeLinkRetry = "moshiro.probe.link_retry";
public const string DialerDisconnect = "moshiro.dialer.disconnect";
public const string LinkRetry = "moshiro.link.retry";
public const string TestStatus = "moshiro.test.status";
public const string Event = "moshiro.event";
public const string Host = "moshiro.host";
@@ -18,7 +18,7 @@ public static class MoShiroMethods
public const string UpdateApply = "moshiro.update.apply";
public const string Schedule = "moshiro.schedule";
// Probe ↔ Agent(点对点会话)
// 拨号方 ↔ 监听方(点对点会话)
public const string Clock = "moshiro.clock";
public const string TestPhase = "moshiro.test.phase";
public const string TestPhaseEnd = "moshiro.test.phase_end";
@@ -27,8 +27,8 @@ public static class MoShiroMethods
public static class AgentEventKinds
{
public const string ProbeConnected = "probe_connected";
public const string ProbeDisconnected = "probe_disconnected";
public const string DialerConnected = "dialer_connected";
public const string DialerDisconnected = "dialer_disconnected";
public const string EndpointChanged = "endpoint_changed";
public const string SessionRejected = "session_rejected";
public const string ClockStep = "clock_step";
@@ -36,7 +36,7 @@ public static class AgentEventKinds
public const string CpuSaturated = "cpu_saturated";
public static readonly IReadOnlyList<string> All =
[ProbeConnected, ProbeDisconnected, EndpointChanged, SessionRejected, ClockStep, Gap, CpuSaturated];
[DialerConnected, DialerDisconnected, EndpointChanged, SessionRejected, ClockStep, Gap, CpuSaturated];
}
public static class TestStatuses
@@ -69,9 +69,9 @@ public sealed class TestRunParams
[JsonPropertyName("test_id")]
public TestId TestId { get; set; }
/// <summary>在哪个 Probe 上运行:必须是当前连着这个 Agent 的 Probe。</summary>
[JsonPropertyName("probe_id")]
public ProbeId ProbeId { get; set; }
/// <summary>在哪个拨号方上运行:必须是当前连着这个监听方的节点。</summary>
[JsonPropertyName("dialer_id")]
public NodeId DialerId { get; set; }
[JsonPropertyName("plan")]
public string Plan { get; set; } = "";
@@ -92,18 +92,18 @@ public sealed class TestCancelParams
public TestId TestId { get; set; }
}
/// <summary>Backend → Agent:断开某个 Probe 的会话(Probe 会按退避重连,除非它已经被吊销)。</summary>
public sealed class ProbeDisconnectParams
/// <summary>Backend → 监听方:断开某个拨号方的会话(拨号方按退避重连,除非它已经被吊销)。</summary>
public sealed class DialerDisconnectParams
{
[JsonPropertyName("probe_id")]
public ProbeId ProbeId { get; set; }
[JsonPropertyName("dialer_id")]
public NodeId DialerId { get; set; }
}
/// <summary>Backend → Probe: immediately retry the selected Agent's current IP paths.</summary>
public sealed class ProbeLinkRetryParams
/// <summary>Backend → 拨号方:立即重试到指定监听方的线路。</summary>
public sealed class LinkRetryParams
{
[JsonPropertyName("agent_id")]
public NodeId AgentId { get; set; }
[JsonPropertyName("listener_id")]
public NodeId ListenerId { get; set; }
}
public sealed class TestStatusParams
@@ -123,9 +123,9 @@ public sealed class TestStatusParams
public sealed class AgentEventParams
{
/// <summary><c>probe_connected</c> 时:这个 Probe 的 id、版本、地址和连接时间(Backend 不用等下一次心跳就能显示)。</summary>
[JsonPropertyName("probe")]
public ConnectedProbe? Probe { get; set; }
/// <summary><c>dialer_connected</c> 时:这个拨号方的 id、版本、地址和连接时间(Backend 不用等下一次心跳就能显示)。</summary>
[JsonPropertyName("dialer")]
public ConnectedDialer? Dialer { get; set; }
[JsonPropertyName("kind")]
public string Kind { get; set; } = "";
@@ -137,13 +137,11 @@ public sealed class AgentEventParams
public string? Detail { get; set; }
}
public sealed class AgentEndpoint
/// <summary>拨号目标:一个监听节点的地址、端口和固定证书用的公钥。</summary>
public sealed class DialTarget
{
[JsonPropertyName("ipv6_host")]
public string? Ipv6Host { get; set; }
[JsonPropertyName("agent_id")]
public NodeId AgentId { get; set; }
[JsonPropertyName("node_id")]
public NodeId NodeId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = "";
@@ -151,46 +149,43 @@ public sealed class AgentEndpoint
[JsonPropertyName("host")]
public string Host { get; set; } = "";
[JsonPropertyName("ipv6_host")]
public string? Ipv6Host { get; set; }
[JsonPropertyName("tcp_port")]
public int TcpPort { get; set; }
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
[JsonPropertyName("udp_ports")]
public List<int> LegacyUdpPorts { get; set; } = [];
/// <summary>Agent 面向 Probe 的 TLS 证书公钥(SPKI,base64):证书按它的 SHA-256 指纹固定,敲门密钥由它和 Probe 的身份密钥派生。</summary>
/// <summary>监听方面向拨号方的 TLS 证书公钥(SPKI,base64):证书按它的 SHA-256 指纹固定,敲门密钥由它和拨号方的身份密钥派生。</summary>
[JsonPropertyName("public_key")]
public string PublicKey { get; set; } = "";
}
/// <summary>期望状态文档(Fleet state type = <c>probe_agents</c>):Backend 直接下发给每个 Probe 的 Agent 列表。</summary>
public sealed class ProbeAgentList
/// <summary>期望状态文档(Fleet state type = <c>dial_targets</c>):Backend 下发给每个节点的、它要拨出的监听节点列表。</summary>
public sealed class DialTargets
{
public const string StateType = "probe_agents";
public const string StateType = "dial_targets";
[JsonPropertyName("version")]
public long Version { get; set; }
[JsonPropertyName("agents")]
public List<AgentEndpoint> Agents { get; set; } = [];
[JsonPropertyName("targets")]
public List<DialTarget> Targets { get; set; } = [];
[JsonPropertyName("trust_chain")]
public List<SignedStatementDto>? TrustChain { get; set; }
}
/// <summary>注册元数据(MoShiro 定义,经 Fleet 的 <c>meta</c> 字段带上来)。</summary>
public sealed class AgentEnrollMeta
/// <summary>注册元数据(MoShiro 定义,经 Fleet 的 <c>meta</c> 字段带上来)。不监听的节点不带端口。</summary>
public sealed class NodeEnrollMeta
{
[JsonPropertyName("probe_tcp_port")]
public int ProbeTcpPort { get; set; } = 7443;
[JsonPropertyName("tcp_port")]
public int TcpPort { get; set; }
[JsonPropertyName("probe_quic_ports")]
public List<int> ProbeQuicPorts { get; set; } = [];
[JsonPropertyName("probe_udp_ports")]
public List<int>? LegacyProbeUdpPorts { get; set; }
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
[JsonPropertyName("public_host")]
public string? PublicHost { get; set; }
@@ -307,19 +302,39 @@ public sealed class UpdateStatus
public long At { get; set; }
}
/// <summary>Probe 的 <c>fleet.heartbeat</c> 附加字段:更新器的状态。</summary>
public sealed class ProbeHeartbeatExtra
/// <summary>节点的 <c>fleet.heartbeat</c> 附加字段:进程内的计数、拨号侧的线路健康和监听侧的会话快照,没有主机信息。</summary>
public sealed class NodeHeartbeatExtra
{
/// <summary>Node operating state; registration approval and transport presence are reported separately.</summary>
[JsonPropertyName("runtime_status")]
public NodeRuntimeStatus RuntimeStatus { get; set; } = NodeRuntimeStatus.Running;
[JsonPropertyName("quic_links")]
public List<ProbeQuicLinkHealth>? QuicLinks { get; set; }
/// <summary>拨号侧:到每个监听节点(每个 IP 族)的线路健康。</summary>
[JsonPropertyName("links")]
public List<LinkHealth>? Links { get; set; }
[JsonPropertyName("ipv6")]
public Ipv6Diagnostic? Ipv6 { get; set; }
/// <summary>监听侧:当前连着的拨号方,每次心跳都是完整的快照,Backend 靠它纠正错过的 dialer_connected / dialer_disconnected 事件。</summary>
[JsonPropertyName("dialers")]
public List<ConnectedDialer> Dialers { get; set; } = [];
[JsonPropertyName("pps_in")]
public long PpsIn { get; set; }
[JsonPropertyName("pps_out")]
public long PpsOut { get; set; }
[JsonPropertyName("dropped_invalid")]
public long DroppedInvalid { get; set; }
[JsonPropertyName("dropped_rate_limited")]
public long DroppedRateLimited { get; set; }
[JsonPropertyName("queue_bytes")]
public long QueueBytes { get; set; }
[JsonPropertyName("update")]
public UpdateStatus? Update { get; set; }
@@ -330,23 +345,94 @@ public sealed class ProbeHeartbeatExtra
public long? DroppedRuntimeLogs { get; set; }
}
/// <summary>Probe's rolling QUIC connection health snapshot for one Agent/IP path.</summary>
public sealed class ProbeQuicLinkHealth
/// <summary>
/// 拨号方到一个监听方(一个 IP 族)的线路健康。控制层(TCP+TLS 长连接,承载控制 RPC 和回传数据)与任务层(测量用的 TCP / QUIC)
/// 各有一个状态机:控制层断开才算两个节点失联,任务层只影响测量。
/// </summary>
public sealed class LinkHealth
{
[JsonPropertyName("agent_id")]
public string AgentId { get; set; } = "";
[JsonPropertyName("listener_id")]
public string ListenerId { get; set; } = "";
[JsonPropertyName("ip_version")]
public string IpVersion { get; set; } = IpVersions.V4;
[JsonPropertyName("connected")]
public bool Connected { get; set; }
[JsonPropertyName("control")]
public ControlHealth Control { get; set; } = new();
[JsonPropertyName("task")]
public TaskHealth Task { get; set; } = new();
/// <summary>控制层在线且 QUIC 数据通道就绪:这条线路可以完整测量。</summary>
[JsonIgnore]
public bool Ready => Control.State == ControlState.Up && Task.Quic.State == QuicState.Ready;
}
/// <summary>某一层最近一次失败的原因:失败发生的阶段、分类和说明。</summary>
public sealed class LinkFault
{
[JsonPropertyName("phase")]
public string Phase { get; set; } = "";
/// <summary><c>timeout</c>、<c>refused</c>、<c>reset</c>、<c>dns</c>、<c>tls</c>、<c>auth</c>、<c>closed</c>、<c>silent</c>、<c>other</c>。</summary>
[JsonPropertyName("kind")]
public string Kind { get; set; } = "other";
[JsonPropertyName("message")]
public string? Message { get; set; }
public override string ToString() => string.IsNullOrEmpty(Message) ? $"{Phase}: {Kind}" : $"{Phase}: {Kind}: {Message}";
}
/// <summary>控制层:到 Agent 的 TCP+TLS 长连接,包括认证、时钟同步,以及回传数据的发件箱。</summary>
public sealed class ControlHealth
{
[JsonPropertyName("state")]
public ProbeLinkAttemptState State { get; set; } = ProbeLinkAttemptState.Connecting;
public ControlState State { get; set; } = ControlState.Dialing;
/// <summary>进入当前状态的时间(Unix 毫秒)。</summary>
[JsonPropertyName("since_ms")]
public long SinceMs { get; set; }
[JsonPropertyName("last_error")]
public string? LastError { get; set; }
public LinkFault? LastError { get; set; }
[JsonPropertyName("retry_attempt")]
public int RetryAttempt { get; set; }
[JsonPropertyName("retry_in_ms")]
public long? RetryInMs { get; set; }
/// <summary>发件箱里还没有被确认的回传数据。</summary>
[JsonPropertyName("outbox_bytes")]
public long OutboxBytes { get; set; }
[JsonPropertyName("outbox_pending")]
public int OutboxPending { get; set; }
}
/// <summary>任务层:测量自己使用的链路。TCP 类积木自带套接字和重拨,QUIC 数据通道由拨号方单独维护。</summary>
public sealed class TaskHealth
{
/// <summary>正在运行的 TCP 类线路积木数(tcp_echo、connect、hold、tcp_bulk)。</summary>
[JsonPropertyName("tcp_blocks")]
public int TcpBlocks { get; set; }
[JsonPropertyName("quic")]
public QuicHealth Quic { get; set; } = new();
}
/// <summary>QUIC 数据通道(udp_echo、udp_train 使用)的状态、握手统计和传输层统计。</summary>
public sealed class QuicHealth
{
[JsonPropertyName("state")]
public QuicState State { get; set; } = QuicState.Idle;
[JsonPropertyName("since_ms")]
public long SinceMs { get; set; }
[JsonPropertyName("last_error")]
public LinkFault? LastError { get; set; }
[JsonPropertyName("retry_attempt")]
public int RetryAttempt { get; set; }
@@ -389,21 +475,40 @@ public sealed class ProbeQuicLinkHealth
public ushort? PathMtu { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter<ProbeLinkAttemptState>))]
public enum ProbeLinkAttemptState
/// <summary>控制层状态:<c>dialing</c>(DNS、TCP、敲门、TLS)→ <c>authenticating</c>(身份签名)→ <c>syncing</c>(时钟)→ <c>up</c>;失败进入 <c>backoff</c>。</summary>
[JsonConverter(typeof(JsonStringEnumConverter<ControlState>))]
public enum ControlState
{
[JsonStringEnumMemberName("dialing")]
Dialing,
[JsonStringEnumMemberName("authenticating")]
Authenticating,
[JsonStringEnumMemberName("syncing")]
Syncing,
[JsonStringEnumMemberName("up")]
Up,
[JsonStringEnumMemberName("backoff")]
Backoff,
}
/// <summary>QUIC 数据通道状态:控制层不在线时是 <c>idle</c>;<c>connecting</c> 握手中;<c>ready</c> 可发送;<c>retrying</c> 断开后等待重拨。</summary>
[JsonConverter(typeof(JsonStringEnumConverter<QuicState>))]
public enum QuicState
{
[JsonStringEnumMemberName("idle")]
Idle,
[JsonStringEnumMemberName("connecting")]
Connecting,
[JsonStringEnumMemberName("retry_wait")]
RetryWait,
[JsonStringEnumMemberName("connected")]
Connected,
[JsonStringEnumMemberName("ready")]
Ready,
[JsonStringEnumMemberName("retrying")]
Retrying,
}
/// <summary>Probe 的 <c>fleet.hello</c> 附加字段:系统、架构和构建信息(只用于展示和更新)。</summary>
public sealed class ProbeHelloMeta
/// <summary>节点的 <c>fleet.hello</c> 附加字段:属主、系统与构建信息,以及(监听时)面向拨号方的接入信息。</summary>
public sealed class NodeHelloMeta
{
/// <summary>这个 Probe 的属主:安装时固定在节点本地的 passkey 凭证 id。面板据此知道它绑定在哪个账户的哪把 passkey 上。</summary>
/// <summary>这个节点的属主:安装时固定在节点本地的 passkey 凭证 id。面板据此知道它绑定在哪个账户的哪把 passkey 上。</summary>
[JsonPropertyName("owners")]
public List<string>? Owners { get; set; }
@@ -412,44 +517,31 @@ public sealed class ProbeHelloMeta
[JsonPropertyName("platform")]
public PlatformInfo? Platform { get; set; }
[JsonPropertyName("local_limits")]
public LocalLimits? LocalLimits { get; set; }
/// <summary>监听接入信息;节点不监听时为 null,面板不会把它当作拨号目标。</summary>
[JsonPropertyName("listen")]
public ListenInfo? Listen { get; set; }
}
/// <summary><c>fleet.hello</c> 的附加字段。</summary>
public sealed class AgentHelloMeta
/// <summary>节点作为监听方时,面向拨号方的接入信息。</summary>
public sealed class ListenInfo
{
/// <summary>这个 Agent 的属主:安装时固定在节点本地的 passkey 凭证 id。面板据此知道它绑定在哪个账户的哪把 passkey 上。</summary>
[JsonPropertyName("owners")]
public List<string>? Owners { get; set; }
[JsonPropertyName("tcp_port")]
public int TcpPort { get; set; }
[JsonPropertyName("ipv6")]
public Ipv6Diagnostic? Ipv6 { get; set; }
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
/// <summary>本机是否启用了 IPv6 监听。旧 Agent 缺省为 false,面板不会给它安排 IPv6 会话。</summary>
/// <summary>本机是否启用了 IPv6 监听;面板只给启用了的节点安排 IPv6 会话。</summary>
[JsonPropertyName("ipv6_supported")]
public bool Ipv6Supported { get; set; }
[JsonPropertyName("platform")]
public PlatformInfo? Platform { get; set; }
/// <summary>Agent 面向 Probe 的 TLS 证书公钥(SPKI,base64);面板据此得到证书指纹,并下发给 Probe 用来固定证书和派生敲门密钥。</summary>
[JsonPropertyName("tls_pubkey")]
public string? TlsPublicKey { get; set; }
/// <summary>本机的公网地址(IPv4 公网地址、IPv6 全球单播地址):面板据此自动填 <c>public_host</c> 和 <c>ipv6_host</c>,管理员手填的优先。</summary>
[JsonPropertyName("addresses")]
public List<string>? Addresses { get; set; }
[JsonPropertyName("probe_tcp_port")]
public int? ProbeTcpPort { get; set; }
[JsonPropertyName("probe_quic_ports")]
public List<int>? ProbeQuicPorts { get; set; }
[JsonPropertyName("probe_udp_ports")]
public List<int>? LegacyProbeUdpPorts { get; set; }
[JsonPropertyName("local_limits")]
public LocalLimits? LocalLimits { get; set; }
}
/// <summary>
@@ -497,19 +589,19 @@ public sealed class HostSnapshot
public long AtUtcMs { get; set; }
}
/// <summary>Agent 眼中的一个已连接 Probe。</summary>
public sealed class ConnectedProbe
/// <summary>监听方眼中的一个已连接拨号方。</summary>
public sealed class ConnectedDialer
{
[JsonPropertyName("ip_version")]
public string IpVersion { get; set; } = IpVersions.V4;
[JsonPropertyName("id")]
public ProbeId Id { get; set; }
public NodeId Id { get; set; }
[JsonPropertyName("version")]
public string? Version { get; set; }
/// <summary>Probe 的远端地址(经过 NAT 之后的公网 IP:端口)。</summary>
/// <summary>拨号方的远端地址(经过 NAT 之后的公网 IP:端口)。</summary>
[JsonPropertyName("remote")]
public string? Remote { get; set; }
@@ -517,50 +609,7 @@ public sealed class ConnectedProbe
public long SinceUtcMs { get; set; }
}
/// <summary><c>fleet.heartbeat</c> 的附加字段:只有进程内的计数,没有主机信息。</summary>
public sealed class AgentHeartbeatExtra
{
/// <summary>Node operating state; registration approval and transport presence are reported separately.</summary>
[JsonPropertyName("runtime_status")]
public NodeRuntimeStatus RuntimeStatus { get; set; } = NodeRuntimeStatus.Running;
[JsonPropertyName("ipv6")]
public Ipv6Diagnostic? Ipv6 { get; set; }
/// <summary>当前连着的 Probe 会话数(等于 <see cref="Probes"/> 的个数)。</summary>
[JsonPropertyName("sessions")]
public int Sessions { get; set; }
/// <summary>当前连着的 Probe:每次心跳都是完整的快照,Backend 靠它纠正错过的 probe_connected / probe_disconnected 事件。</summary>
[JsonPropertyName("probes")]
public List<ConnectedProbe> Probes { get; set; } = [];
[JsonPropertyName("pps_in")]
public long PpsIn { get; set; }
[JsonPropertyName("pps_out")]
public long PpsOut { get; set; }
[JsonPropertyName("dropped_invalid")]
public long DroppedInvalid { get; set; }
[JsonPropertyName("dropped_rate_limited")]
public long DroppedRateLimited { get; set; }
[JsonPropertyName("queue_bytes")]
public long QueueBytes { get; set; }
[JsonPropertyName("update")]
public UpdateStatus? Update { get; set; }
[JsonPropertyName("runtime_logs")]
public List<NodeLogRecord>? RuntimeLogs { get; set; }
[JsonPropertyName("dropped_runtime_logs")]
public long? DroppedRuntimeLogs { get; set; }
}
/// <summary>Agent 下发给 Probe 的一条线路任务(期望状态类型 <c>line_task_set</c> 里的一项)。</summary>
/// <summary>监听方下发给拨号方的一条线路任务(期望状态类型 <c>line_task_set</c> 里的一项)。</summary>
public sealed class LineTask
{
[JsonPropertyName("id")]
@@ -580,7 +629,7 @@ public sealed class LineTask
public int Port { get; set; }
}
/// <summary>这条线路的期望状态(Agent → Probe):Probe 按它调和自己一侧的角色。</summary>
/// <summary>这条线路的期望状态(监听方 → 拨号方):拨号方按它调和自己一侧的角色。</summary>
public sealed class LineTaskSet
{
public const string StateType = "line_task_set";
@@ -591,31 +640,28 @@ public sealed class LineTaskSet
[JsonPropertyName("tasks")]
public List<LineTask> Tasks { get; set; } = [];
/// <summary>Agent 的数据面端口(Probe 把包发到这里)。</summary>
/// <summary>监听方的数据面端口(拨号方把包发到这里)。</summary>
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
[JsonPropertyName("udp_ports")]
public List<int> LegacyUdpPorts { get; set; } = [];
[JsonPropertyName("tcp_ports")]
public List<int> TcpPorts { get; set; } = [];
}
/// <summary><c>moshiro.clock</c>:Probe 用它估计与 Agent 单调时钟的偏移(对称往返假设)。</summary>
/// <summary><c>moshiro.clock</c>:拨号方用它估计与监听方单调时钟的偏移(对称往返假设)。</summary>
public sealed class ClockParams
{
[JsonPropertyName("probe_mono_us")]
public long ProbeMonoUs { get; set; }
[JsonPropertyName("dialer_mono_us")]
public long DialerMonoUs { get; set; }
}
public sealed class ClockResult
{
[JsonPropertyName("agent_mono_us")]
public long AgentMonoUs { get; set; }
[JsonPropertyName("listener_mono_us")]
public long ListenerMonoUs { get; set; }
}
/// <summary>按需测试的一个阶段(Agent → Probe):Agent 编排时间,Probe 准备好自己一侧的角色之后应答。</summary>
/// <summary>按需测试的一个阶段(监听方 → 拨号方):监听方编排时间,拨号方准备好自己一侧的角色之后应答。</summary>
public sealed class TestPhaseStartParams
{
[JsonPropertyName("test_id")]
@@ -646,7 +692,7 @@ public sealed class TestPhaseStartResult
public bool Accepted { get; set; }
}
/// <summary>阶段结束(Agent → Probe):Probe 收尾并把自己一侧的列块交给 Agent。<c>sent_count</c> 是 Agent 一侧发出的包数(单向数据流用)。</summary>
/// <summary>阶段结束(监听方 → 拨号方):拨号方收尾并把自己一侧的列块交给监听方。<c>sent_count</c> 是监听方一侧发出的包数(单向数据流用)。</summary>
public sealed class TestPhaseEndParams
{
[JsonPropertyName("test_id")]
@@ -662,7 +708,7 @@ public sealed class TestPhaseEndParams
public long SentCount { get; set; }
}
/// <summary>Probe 收尾之后回复:它交给 Agent 的列块个数(Agent 据此等待全部列块到齐再进入下一阶段)。</summary>
/// <summary>拨号方收尾之后回复:它交给监听方的列块个数(监听方据此等待全部列块到齐再进入下一阶段)。</summary>
public sealed class TestPhaseEndResult
{
[JsonPropertyName("fragments")]
@@ -675,7 +721,7 @@ public sealed class TestAbortParams
public TestId TestId { get; set; }
}
/// <summary>Probe 一侧的列块放在 RawChunk 头部 params 里的保留键:本块覆盖的序号范围 [from, to)(单向数据流由 Agent 一侧按它取行)。</summary>
/// <summary>拨号方一侧的列块放在 RawChunk 头部 params 里的保留键:本块覆盖的序号范围 [from, to)(单向数据流由监听方一侧按它取行)。</summary>
public static class FragmentKeys
{
public const string SeqFrom = "_seq_from";
@@ -695,21 +741,19 @@ public static class FragmentKeys
[JsonSerializable(typeof(TestRunParams))]
[JsonSerializable(typeof(TestRunResult))]
[JsonSerializable(typeof(TestCancelParams))]
[JsonSerializable(typeof(ProbeDisconnectParams))]
[JsonSerializable(typeof(ProbeLinkRetryParams))]
[JsonSerializable(typeof(DialerDisconnectParams))]
[JsonSerializable(typeof(LinkRetryParams))]
[JsonSerializable(typeof(HostSnapshot))]
[JsonSerializable(typeof(TestStatusParams))]
[JsonSerializable(typeof(AgentEventParams))]
[JsonSerializable(typeof(ProbeAgentList))]
[JsonSerializable(typeof(DialTargets))]
[JsonSerializable(typeof(UpdateOfferParams))]
[JsonSerializable(typeof(ScheduleParams))]
[JsonSerializable(typeof(UpdateStatus))]
[JsonSerializable(typeof(ProbeHeartbeatExtra))]
[JsonSerializable(typeof(ProbeHelloMeta))]
[JsonSerializable(typeof(AgentEnrollMeta))]
[JsonSerializable(typeof(NodeHeartbeatExtra))]
[JsonSerializable(typeof(NodeHelloMeta))]
[JsonSerializable(typeof(NodeEnrollMeta))]
[JsonSerializable(typeof(PlatformInfo))]
[JsonSerializable(typeof(AgentHelloMeta))]
[JsonSerializable(typeof(Ipv6Diagnostic))]
[JsonSerializable(typeof(AgentHeartbeatExtra))]
[JsonSerializable(typeof(RawChunkHeader))]
public sealed partial class MoShiroJsonContext : JsonSerializerContext;
@@ -10,8 +10,8 @@ public enum NodeRuntimeStatus
Initializing,
[JsonStringEnumMemberName("running")]
Running,
[JsonStringEnumMemberName("probe_paused")]
ProbePaused,
[JsonStringEnumMemberName("idle")]
Idle,
[JsonStringEnumMemberName("maintenance")]
Maintenance,
[JsonStringEnumMemberName("stopping")]