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:
1 parent
d5ebf29e57
commit
ad5efc62ff
206 files changed
+4202
-4964
No files matched your search
@@ -23,37 +23,37 @@ public enum ColumnEnc
|
||||
Delta,
|
||||
}
|
||||
|
||||
/// <summary>时间列的时钟归属:Probe、Agent 各自的单调时钟(微秒),或所在一侧的本地时钟。</summary>
|
||||
/// <summary>时间列的时钟归属:拨号方、监听方各自的单调时钟(微秒),或所在一侧的本地时钟。</summary>
|
||||
public enum ColumnRole
|
||||
{
|
||||
None,
|
||||
TsProbe,
|
||||
TsAgent,
|
||||
TsDialer,
|
||||
TsListener,
|
||||
TsLocal,
|
||||
}
|
||||
|
||||
public sealed record ColumnSpec(string Name, ColumnType Type, ColumnEnc Enc = ColumnEnc.Plain, bool Nullable = false, ColumnRole Role = ColumnRole.None);
|
||||
|
||||
/// <summary>数据面上的一侧:Probe 永远是拨号方,Agent 永远是监听方。</summary>
|
||||
/// <summary>数据面上的一侧:拨号方发起连接,监听方接受连接。</summary>
|
||||
public enum Side
|
||||
{
|
||||
Probe,
|
||||
Agent,
|
||||
Dialer,
|
||||
Listener,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一个记录流(一张表)。<see cref="Driver"/> 是行的主导方:行由它发出的包或事件决定,另一侧的列按 <see cref="Key"/>(例如 <c>seq</c>)补充,
|
||||
/// 缺失就是缺失(回显缺 t2/t3 表示上行丢失,单向数据流缺 t_recv 表示丢失)。没有 <see cref="Key"/> 的流只有主导方一侧的列,原样透传。
|
||||
/// 每一列属于哪一侧由它的时钟决定(<c>TsProbe</c> 属于 Probe,<c>TsAgent</c> 属于 Agent),其他列属于主导方。
|
||||
/// 每一列属于哪一侧由它的时钟决定(<c>TsDialer</c> 属于拨号方,<c>TsListener</c> 属于监听方),其他列属于主导方。
|
||||
/// <see cref="Union"/> 流是例外:两侧各自记录完整的行(行里有一列标明是哪一侧),合并时直接拼接,不连接。
|
||||
/// 这一条声明同时决定了两端各记录什么、Agent 怎么合并,新增一种记录流不需要写合并代码。
|
||||
/// 这一条声明同时决定了两端各记录什么、监听方怎么合并,新增一种记录流不需要写合并代码。
|
||||
/// </summary>
|
||||
public sealed record StreamSpec(string Name, IReadOnlyList<ColumnSpec> Columns, Side Driver = Side.Probe, string? Key = null, bool Union = false)
|
||||
public sealed record StreamSpec(string Name, IReadOnlyList<ColumnSpec> Columns, Side Driver = Side.Dialer, string? Key = null, bool Union = false)
|
||||
{
|
||||
public Side OwnerOf(ColumnSpec column) => column.Role switch
|
||||
{
|
||||
ColumnRole.TsProbe => Side.Probe,
|
||||
ColumnRole.TsAgent => Side.Agent,
|
||||
ColumnRole.TsDialer => Side.Dialer,
|
||||
ColumnRole.TsListener => Side.Listener,
|
||||
_ => Driver,
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed record BlockSet(string Transport, string Pacer, string Payload, st
|
||||
|
||||
/// <summary>
|
||||
/// 采集方法声明:积木组合 + 参数 + 记录的列 + 带宽预算。
|
||||
/// 这一条声明同时被 Probe/Agent(组装执行)、Backend(校验与目录)、Analysis(按列名读数据)使用。
|
||||
/// 这一条声明同时被拨号方和监听方(组装执行)、Backend(校验与目录)、Analysis(按列名读数据)使用。
|
||||
/// </summary>
|
||||
public sealed record KindSpec(
|
||||
string Id,
|
||||
@@ -109,9 +109,9 @@ public static class Col
|
||||
/// <summary>单调递增的序号:差分编码。</summary>
|
||||
public static ColumnSpec Seq(string name) => new(name, ColumnType.U32, ColumnEnc.Delta);
|
||||
|
||||
public static ColumnSpec TsProbe(string name, bool nullable = false) => new(name, ColumnType.I64, ColumnEnc.Delta, nullable, ColumnRole.TsProbe);
|
||||
public static ColumnSpec TsDialer(string name, bool nullable = false) => new(name, ColumnType.I64, ColumnEnc.Delta, nullable, ColumnRole.TsDialer);
|
||||
|
||||
public static ColumnSpec TsAgent(string name, bool nullable = false) => new(name, ColumnType.I64, ColumnEnc.Delta, nullable, ColumnRole.TsAgent);
|
||||
public static ColumnSpec TsListener(string name, bool nullable = false) => new(name, ColumnType.I64, ColumnEnc.Delta, nullable, ColumnRole.TsListener);
|
||||
|
||||
public static ColumnSpec TsLocal(string name) => new(name, ColumnType.I64, ColumnEnc.Delta, false, ColumnRole.TsLocal);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ public static class Kinds
|
||||
"msg",
|
||||
[
|
||||
Col.Seq("seq"), Col.U16("size"),
|
||||
Col.TsProbe("t1"), Col.TsAgent("t2", nullable: true), Col.TsAgent("t3", nullable: true), Col.TsProbe("t4", nullable: true),
|
||||
Col.TsDialer("t1"), Col.TsListener("t2", nullable: true), Col.TsListener("t3", nullable: true), Col.TsDialer("t4", nullable: true),
|
||||
],
|
||||
Side.Probe,
|
||||
Side.Dialer,
|
||||
"seq");
|
||||
|
||||
private static readonly StreamSpec s_tcpInfo = new(
|
||||
@@ -43,7 +43,7 @@ public static class Kinds
|
||||
public static readonly KindSpec UdpTrain = new(
|
||||
"udp_train",
|
||||
"QUIC Datagram train",
|
||||
"Fixed-rate or laddered one-way QUIC Datagram stream (Agent to Probe) for loss and delay variation under load.",
|
||||
"Fixed-rate or laddered one-way QUIC Datagram stream (Agent to 拨号方) for loss and delay variation under load.",
|
||||
Scope.Line,
|
||||
new BlockSet("quic_datagram", "ladder", "fixed", "sink", []),
|
||||
[
|
||||
@@ -54,8 +54,8 @@ public static class Kinds
|
||||
P.Int("port", 7443, 1, 65535),
|
||||
],
|
||||
[
|
||||
new StreamSpec("pkt", [Col.Seq("seq"), Col.U16("size"), Col.TsAgent("t_send"), Col.TsProbe("t_recv", nullable: true)], Side.Agent, "seq"),
|
||||
new StreamSpec("step", [Col.U16("index"), Col.U32("mbps"), Col.TsProbe("start_ts"), Col.TsProbe("end_ts")]),
|
||||
new StreamSpec("pkt", [Col.Seq("seq"), Col.U16("size"), Col.TsListener("t_send"), Col.TsDialer("t_recv", nullable: true)], Side.Listener, "seq"),
|
||||
new StreamSpec("step", [Col.U16("index"), Col.U32("mbps"), Col.TsDialer("start_ts"), Col.TsDialer("end_ts")]),
|
||||
],
|
||||
p => (long)(p.GetNumber("mbps") * 1_000_000));
|
||||
|
||||
@@ -91,7 +91,7 @@ public static class Kinds
|
||||
P.Bool("tls", true),
|
||||
],
|
||||
[
|
||||
new StreamSpec("bytes", [Col.TsProbe("ts"), Col.U8("conn"), Col.U64("bytes")]),
|
||||
new StreamSpec("bytes", [Col.TsDialer("ts"), Col.U8("conn"), Col.U64("bytes")]),
|
||||
s_tcpInfo,
|
||||
],
|
||||
_ => 0);
|
||||
@@ -111,7 +111,7 @@ public static class Kinds
|
||||
[
|
||||
new StreamSpec(
|
||||
"conn",
|
||||
[Col.Seq("attempt"), Col.TsProbe("ts"), Col.U32("tcp_us", nullable: true), Col.U32("tls_us", nullable: true), Col.U8("outcome")]),
|
||||
[Col.Seq("attempt"), Col.TsDialer("ts"), Col.U32("tcp_us", nullable: true), Col.U32("tls_us", nullable: true), Col.U8("outcome")]),
|
||||
],
|
||||
p => p.GetInt("per_minute") * 4000 / 60);
|
||||
|
||||
@@ -126,14 +126,14 @@ public static class Kinds
|
||||
P.Int("port", 7444, 1, 65535),
|
||||
],
|
||||
[
|
||||
new StreamSpec("hold", [Col.TsProbe("ts"), Col.U8("event"), Col.U32("lifetime_ms")]),
|
||||
new StreamSpec("hold", [Col.TsDialer("ts"), Col.U8("event"), Col.U32("lifetime_ms")]),
|
||||
],
|
||||
p => 2000 / Math.Max(1, p.GetInt("heartbeat_s")) * 8);
|
||||
|
||||
public static readonly KindSpec IcmpEcho = new(
|
||||
"icmp_echo",
|
||||
"ICMP echo",
|
||||
"Probe or Agent side ICMP echo to a target address.",
|
||||
"拨号方 or Agent side ICMP echo to a target address.",
|
||||
Scope.Target,
|
||||
new BlockSet("icmp", "fixed", "fixed", "echo", []),
|
||||
[
|
||||
|
||||
@@ -3,14 +3,6 @@ using Fleet.Core;
|
||||
|
||||
namespace MoShiro.Protocol;
|
||||
|
||||
[JsonConverter(typeof(ProbeIdJsonConverter))]
|
||||
public readonly record struct ProbeId(string Value)
|
||||
{
|
||||
public const string Prefix = "prb";
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(TaskIdJsonConverter))]
|
||||
public readonly record struct TaskId(string Value)
|
||||
{
|
||||
@@ -42,13 +34,6 @@ public static class AgentIds
|
||||
public const string Prefix = "agt";
|
||||
}
|
||||
|
||||
public sealed class ProbeIdJsonConverter : StringIdJsonConverter<ProbeId>
|
||||
{
|
||||
protected override ProbeId Create(string value) => new(value);
|
||||
|
||||
protected override string GetValue(ProbeId id) => id.Value;
|
||||
}
|
||||
|
||||
public sealed class TaskIdJsonConverter : StringIdJsonConverter<TaskId>
|
||||
{
|
||||
protected override TaskId Create(string value) => new(value);
|
||||
|
||||
@@ -8,6 +8,6 @@ public static class IpVersions
|
||||
|
||||
public static bool IsValid(string? value) => value is V4 or V6;
|
||||
|
||||
public static string LineSeries(string probeId, string ipVersion) =>
|
||||
RawChunkHeader.LinePrefix + probeId + (ipVersion == V6 ? ":ipv6" : "");
|
||||
public static string LineSeries(string dialerId, string ipVersion) =>
|
||||
RawChunkHeader.LinePrefix + dialerId + (ipVersion == V6 ? ":ipv6" : "");
|
||||
}
|
||||
@@ -13,9 +13,10 @@ public static class MoShiroErrors
|
||||
public static readonly ErrorCode SitePrivate = new("site_private");
|
||||
public static readonly ErrorCode NotFound = new("not_found");
|
||||
public static readonly ErrorCode AgentNotFound = new("agent_not_found");
|
||||
public static readonly ErrorCode ProbeNotFound = new("probe_not_found");
|
||||
public static readonly ErrorCode ProbeRevoked = new("probe_revoked");
|
||||
public static readonly ErrorCode ProbeRequired = new("probe_required");
|
||||
public static readonly ErrorCode NodeRevoked = new("node_revoked");
|
||||
public static readonly ErrorCode DialerRequired = new("dialer_required");
|
||||
public static readonly ErrorCode NotListening = new("not_listening");
|
||||
public static readonly ErrorCode SelfLink = new("self_link");
|
||||
public static readonly ErrorCode TaskNotFound = new("task_not_found");
|
||||
public static readonly ErrorCode TestNotFound = new("test_not_found");
|
||||
public static readonly ErrorCode ChunkNotFound = new("chunk_not_found");
|
||||
@@ -23,7 +24,7 @@ public static class MoShiroErrors
|
||||
public static readonly ErrorCode UnknownKind = new("unknown_kind");
|
||||
public static readonly ErrorCode UnknownPlan = new("unknown_plan");
|
||||
public static readonly ErrorCode NameConflict = new("name_conflict");
|
||||
public static readonly ErrorCode ProbeOffline = new("probe_offline");
|
||||
public static readonly ErrorCode DialerOffline = new("dialer_offline");
|
||||
public static readonly ErrorCode TestBusy = new("test_busy");
|
||||
public static readonly ErrorCode AgentNotActive = new("agent_not_active");
|
||||
public static readonly ErrorCode AgentOffline = new("agent_offline");
|
||||
|
||||
@@ -30,7 +30,7 @@ public static class MoShiroChannels
|
||||
/// <summary>Agent → Backend:RawChunk(发件箱)。</summary>
|
||||
public const byte RawChunk = 16;
|
||||
|
||||
/// <summary>Probe → Agent:列块(点对点会话的发件箱)。</summary>
|
||||
/// <summary>拨号方 → Agent:列块(点对点会话的发件箱)。</summary>
|
||||
public const byte ColumnBlock = 17;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,22 +4,22 @@ using Fleet.Core;
|
||||
|
||||
namespace MoShiro.Protocol;
|
||||
|
||||
/// <summary>Agent 单调时钟与 UTC 的锚点,以及 Probe 时钟相对 Agent 时钟的偏移估计。</summary>
|
||||
/// <summary>监听方单调时钟与 UTC 的锚点,以及拨号方时钟相对监听方时钟的偏移估计。</summary>
|
||||
public sealed class ChunkClock
|
||||
{
|
||||
[JsonPropertyName("agent_mono_us")]
|
||||
public long AgentMonoUs { get; set; }
|
||||
[JsonPropertyName("listener_mono_us")]
|
||||
public long ListenerMonoUs { get; set; }
|
||||
|
||||
[JsonPropertyName("utc_ms")]
|
||||
public long UtcMs { get; set; }
|
||||
|
||||
/// <summary>Agent 时钟 − Probe 时钟(微秒)。</summary>
|
||||
[JsonPropertyName("probe_offset_us")]
|
||||
public long ProbeOffsetUs { get; set; }
|
||||
/// <summary>监听方时钟 − 拨号方时钟(微秒)。</summary>
|
||||
[JsonPropertyName("dialer_offset_us")]
|
||||
public long DialerOffsetUs { get; set; }
|
||||
|
||||
/// <summary>Probe 相对 Agent 的时钟漂移(ppb,十亿分之一)。</summary>
|
||||
[JsonPropertyName("probe_drift_ppb")]
|
||||
public long ProbeDriftPpb { get; set; }
|
||||
/// <summary>拨号方相对监听方的时钟漂移(ppb,十亿分之一)。</summary>
|
||||
[JsonPropertyName("dialer_drift_ppb")]
|
||||
public long DialerDriftPpb { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ColumnHeader
|
||||
@@ -38,7 +38,7 @@ public sealed class ColumnHeader
|
||||
[JsonPropertyName("nullable")]
|
||||
public bool Nullable { get; set; }
|
||||
|
||||
/// <summary>ts_probe | ts_agent | ts_local,其他为空。</summary>
|
||||
/// <summary>ts_dialer | ts_listener | ts_local,其他为空。</summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role { get; set; }
|
||||
}
|
||||
@@ -83,8 +83,8 @@ public sealed class RawChunkHeader
|
||||
[JsonPropertyName("agent_id")]
|
||||
public NodeId AgentId { get; set; }
|
||||
|
||||
[JsonPropertyName("probe_id")]
|
||||
public ProbeId? ProbeId { get; set; }
|
||||
[JsonPropertyName("dialer_id")]
|
||||
public NodeId? DialerId { get; set; }
|
||||
|
||||
[JsonPropertyName("target")]
|
||||
public string? Target { get; set; }
|
||||
@@ -105,7 +105,7 @@ public sealed class RawChunkHeader
|
||||
[JsonPropertyName("duration_ms")]
|
||||
public int DurationMs { get; set; }
|
||||
|
||||
/// <summary>Probe 一侧的列没有在宽限期内送达:只有 Agent 一侧的数据。</summary>
|
||||
/// <summary>拨号方一侧的列没有在宽限期内送达:只有监听方一侧的数据。</summary>
|
||||
[JsonPropertyName("partial")]
|
||||
public bool Partial { get; set; }
|
||||
|
||||
@@ -116,11 +116,11 @@ public sealed class RawChunkHeader
|
||||
public List<StreamHeader> Streams { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// "line:<probe_id>" 或 "target:<task_id>":存储和分析用的序列标识。
|
||||
/// 同一个 Agent 上每个 Probe 的每种地址族各是一个序列,IPv6 追加 :ipv6,互不混合。
|
||||
/// "line:<dialer_id>" 或 "target:<task_id>":存储和分析用的序列标识。
|
||||
/// 同一个监听节点上每个拨号方的每种地址族各是一个序列,IPv6 追加 :ipv6,互不混合。
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Series => TaskId is { } id && Target is not null ? "target:" + id.Value : IpVersions.LineSeries(ProbeId?.Value ?? "", IpVersion);
|
||||
public string Series => TaskId is { } id && Target is not null ? "target:" + id.Value : IpVersions.LineSeries(DialerId?.Value ?? "", IpVersion);
|
||||
|
||||
public const string LinePrefix = "line:";
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Fleet.Core;
|
||||
namespace MoShiro.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// RawChunk 的编解码(Probe、Agent、Backend、离线工具共用同一份代码)。
|
||||
/// RawChunk 的编解码(拨号方、Agent、Backend、离线工具共用同一份代码)。
|
||||
/// 布局:<c>"MSRC" | u8 container_version | u32 crc32c(压缩数据) | Brotli( u32 header_len | header JSON | 各列数据 )</c>。
|
||||
/// 列数据:可空列先写有效位图,再写有效值;plain 为无符号 varint(有符号类型用 zigzag),delta 为与上一个有效值之差的 zigzag varint。
|
||||
/// </summary>
|
||||
@@ -334,16 +334,16 @@ public static class RawChunkCodec
|
||||
|
||||
private static string? RoleName(ColumnRole role) => role switch
|
||||
{
|
||||
ColumnRole.TsProbe => "ts_probe",
|
||||
ColumnRole.TsAgent => "ts_agent",
|
||||
ColumnRole.TsDialer => "ts_dialer",
|
||||
ColumnRole.TsListener => "ts_listener",
|
||||
ColumnRole.TsLocal => "ts_local",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static ColumnRole ParseRole(string? text) => text switch
|
||||
{
|
||||
"ts_probe" => ColumnRole.TsProbe,
|
||||
"ts_agent" => ColumnRole.TsAgent,
|
||||
"ts_dialer" => ColumnRole.TsDialer,
|
||||
"ts_listener" => ColumnRole.TsListener,
|
||||
"ts_local" => ColumnRole.TsLocal,
|
||||
_ => ColumnRole.None,
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
]));
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -34,10 +34,10 @@ public sealed class TaskSpec
|
||||
|
||||
public sealed class PeerSpec
|
||||
{
|
||||
[JsonPropertyName("probe_id")]
|
||||
public ProbeId ProbeId { get; set; }
|
||||
[JsonPropertyName("node_id")]
|
||||
public NodeId NodeId { get; set; }
|
||||
|
||||
/// <summary>Probe 的身份公钥(SPKI,base64)。</summary>
|
||||
/// <summary>拨号方的身份公钥(SPKI,base64)。</summary>
|
||||
[JsonPropertyName("pubkey")]
|
||||
public string PublicKey { get; set; } = "";
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public sealed class TaskSet
|
||||
[JsonPropertyName("tasks")]
|
||||
public List<TaskSpec> Tasks { get; set; } = [];
|
||||
|
||||
/// <summary>允许连接到这个 Agent 的 Probe 公钥。</summary>
|
||||
/// <summary>允许拨入这个节点的拨号方公钥。</summary>
|
||||
[JsonPropertyName("peers")]
|
||||
public List<PeerSpec> Peers { get; set; } = [];
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ public enum MeasurementDatagramType : byte
|
||||
EchoRequest = 1,
|
||||
EchoReply = 2,
|
||||
|
||||
/// <summary>单向数据(udp_train),以及 Probe 为了保持 NAT 映射和让 Agent 学到源地址而发的探活包。</summary>
|
||||
/// <summary>单向数据(udp_train),以及 拨号方 为了保持 NAT 映射和让 Agent 学到源地址而发的探活包。</summary>
|
||||
Data = 3,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using Fleet.Core;
|
||||
namespace MoShiro.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// 数据面的 TCP 连接(Fleet 之外的线格式)。连接都由 Probe 发起;TLS 是可选的(ClientHello 以 0x16 开头,
|
||||
/// 数据面的 TCP 连接(Fleet 之外的线格式)。连接都由 拨号方 发起;TLS 是可选的(ClientHello 以 0x16 开头,
|
||||
/// 明文首帧以 "MN" 开头,所以同一个端口可以区分)。首帧:
|
||||
/// <c>"MN"(2) version(1) session_id(8) stream_id(4) nonce(16) mac(16)</c>,认证失败直接关闭连接;
|
||||
/// 之后是 <c>VarInt 长度 | 正文</c> 的消息,回显类消息的正文是 <c>seq(u32) send_ts_us(i64) | padding</c>。
|
||||
|
||||
Reference in new issue
Block a user