837 lines
28 KiB
C#
837 lines
28 KiB
C#
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
using Fleet.Core;
|
||
|
||
namespace MoShiro.Protocol;
|
||
|
||
/// <summary>moshiro.* 方法名。新增方法 = 在这里定义名称和 params/result,再在组装处加一行 <c>.On(...)</c>。</summary>
|
||
public static class MoShiroMethods
|
||
{
|
||
public const string TestRun = "moshiro.test.run";
|
||
public const string TestCancel = "moshiro.test.cancel";
|
||
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";
|
||
public const string UpdateOffer = "moshiro.update.offer";
|
||
public const string UpdateApply = "moshiro.update.apply";
|
||
public const string Schedule = "moshiro.schedule";
|
||
public const string LogsFetch = "moshiro.logs.fetch";
|
||
|
||
// 拨号方 ↔ 监听方(点对点会话)
|
||
public const string Clock = "moshiro.clock";
|
||
public const string TestPhase = "moshiro.test.phase";
|
||
public const string TestPhaseEnd = "moshiro.test.phase_end";
|
||
public const string TestAbort = "moshiro.test.abort";
|
||
}
|
||
|
||
public static class AgentEventKinds
|
||
{
|
||
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";
|
||
public const string Gap = "gap";
|
||
public const string CpuSaturated = "cpu_saturated";
|
||
|
||
public static readonly IReadOnlyList<string> All =
|
||
[DialerConnected, DialerDisconnected, EndpointChanged, SessionRejected, ClockStep, Gap, CpuSaturated];
|
||
}
|
||
|
||
public static class TestStatuses
|
||
{
|
||
public const string Queued = "queued";
|
||
public const string Running = "running";
|
||
public const string PhaseDone = "phase_done";
|
||
public const string Succeeded = "succeeded";
|
||
public const string Failed = "failed";
|
||
public const string Aborted = "aborted";
|
||
}
|
||
|
||
public sealed class TestPhase
|
||
{
|
||
[JsonPropertyName("kind")]
|
||
public string Kind { get; set; } = "";
|
||
|
||
[JsonPropertyName("params")]
|
||
public Dictionary<string, JsonElement> Params { get; set; } = [];
|
||
|
||
[JsonPropertyName("duration_s")]
|
||
public int DurationSeconds { get; set; }
|
||
}
|
||
|
||
public sealed class TestRunParams
|
||
{
|
||
[JsonPropertyName("ip_version")]
|
||
public string IpVersion { get; set; } = IpVersions.V4;
|
||
|
||
[JsonPropertyName("test_id")]
|
||
public TestId TestId { get; set; }
|
||
|
||
/// <summary>在哪个拨号方上运行:必须是当前连着这个监听方的节点。</summary>
|
||
[JsonPropertyName("dialer_id")]
|
||
public NodeId DialerId { get; set; }
|
||
|
||
[JsonPropertyName("plan")]
|
||
public string Plan { get; set; } = "";
|
||
|
||
[JsonPropertyName("phases")]
|
||
public List<TestPhase> Phases { get; set; } = [];
|
||
}
|
||
|
||
public sealed class TestRunResult
|
||
{
|
||
[JsonPropertyName("accepted")]
|
||
public bool Accepted { get; set; }
|
||
}
|
||
|
||
public sealed class TestCancelParams
|
||
{
|
||
[JsonPropertyName("test_id")]
|
||
public TestId TestId { get; set; }
|
||
}
|
||
|
||
/// <summary>Backend → 监听方:断开某个拨号方的会话(拨号方按退避重连,除非它已经被吊销)。</summary>
|
||
public sealed class DialerDisconnectParams
|
||
{
|
||
[JsonPropertyName("dialer_id")]
|
||
public NodeId DialerId { get; set; }
|
||
}
|
||
|
||
/// <summary>Backend → 拨号方:立即重试到指定监听方的线路。</summary>
|
||
public sealed class LinkRetryParams
|
||
{
|
||
[JsonPropertyName("listener_id")]
|
||
public NodeId ListenerId { get; set; }
|
||
}
|
||
|
||
public sealed class TestStatusParams
|
||
{
|
||
[JsonPropertyName("test_id")]
|
||
public TestId TestId { get; set; }
|
||
|
||
[JsonPropertyName("status")]
|
||
public string Status { get; set; } = "";
|
||
|
||
[JsonPropertyName("phase")]
|
||
public int? Phase { get; set; }
|
||
|
||
[JsonPropertyName("error_code")]
|
||
public string? ErrorCode { get; set; }
|
||
}
|
||
|
||
public sealed class AgentEventParams
|
||
{
|
||
/// <summary><c>dialer_connected</c> 时:这个拨号方的 id、版本、地址和连接时间(Backend 不用等下一次心跳就能显示)。</summary>
|
||
[JsonPropertyName("dialer")]
|
||
public ConnectedDialer? Dialer { get; set; }
|
||
|
||
[JsonPropertyName("kind")]
|
||
public string Kind { get; set; } = "";
|
||
|
||
[JsonPropertyName("at_utc_ms")]
|
||
public long AtUtcMs { get; set; }
|
||
|
||
[JsonPropertyName("detail")]
|
||
public string? Detail { get; set; }
|
||
}
|
||
|
||
/// <summary>拨号目标:一个监听节点的地址、端口和固定证书用的公钥。</summary>
|
||
public sealed class DialTarget
|
||
{
|
||
[JsonPropertyName("node_id")]
|
||
public NodeId NodeId { get; set; }
|
||
|
||
[JsonPropertyName("name")]
|
||
public string Name { get; set; } = "";
|
||
|
||
[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; } = [];
|
||
|
||
/// <summary>监听方面向拨号方的 TLS 证书公钥(SPKI,base64):证书按它的 SHA-256 指纹固定,敲门密钥由它和拨号方的身份密钥派生。</summary>
|
||
[JsonPropertyName("public_key")]
|
||
public string PublicKey { get; set; } = "";
|
||
}
|
||
|
||
/// <summary>期望状态文档(Fleet state type = <c>dial_targets</c>):Backend 下发给每个节点的、它要拨出的监听节点列表。</summary>
|
||
public sealed class DialTargets
|
||
{
|
||
public const string StateType = "dial_targets";
|
||
|
||
[JsonPropertyName("version")]
|
||
public long Version { 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 NodeEnrollMeta
|
||
{
|
||
[JsonPropertyName("tcp_port")]
|
||
public int TcpPort { get; set; }
|
||
|
||
[JsonPropertyName("quic_ports")]
|
||
public List<int> QuicPorts { get; set; } = [];
|
||
|
||
[JsonPropertyName("public_host")]
|
||
public string? PublicHost { get; set; }
|
||
|
||
[JsonPropertyName("kinds")]
|
||
public List<string> Kinds { get; set; } = [];
|
||
}
|
||
|
||
public sealed class LocalLimits
|
||
{
|
||
[JsonPropertyName("max_total_mbps")]
|
||
public double MaxTotalMbps { get; set; }
|
||
|
||
[JsonPropertyName("max_total_pps")]
|
||
public long MaxTotalPps { get; set; }
|
||
|
||
[JsonPropertyName("allowed_target_cidrs")]
|
||
public List<string> AllowedTargetCidrs { get; set; } = [];
|
||
|
||
[JsonPropertyName("max_concurrent_tests")]
|
||
public int MaxConcurrentTests { get; set; } = 1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 节点的系统和架构(粗粒度,只用于展示):<c>os</c> 是 linux / windows / macos / other,<c>name</c> 是「Debian 12」「Windows 11」这样的名字,
|
||
/// <c>arch</c> 是 x64 / arm64 / arm / x86。<b>没有主机名、内核版本、具体型号</b>。另外带着这个程序自己的构建信息(版本、tag、构建时间)。
|
||
/// </summary>
|
||
public sealed class PlatformInfo
|
||
{
|
||
[JsonPropertyName("os")]
|
||
public string Os { get; set; } = "other";
|
||
|
||
[JsonPropertyName("name")]
|
||
public string? Name { get; set; }
|
||
|
||
[JsonPropertyName("arch")]
|
||
public string? Arch { get; set; }
|
||
|
||
/// <summary>程序的版本、tag、提交(短)和构建时间(UTC,ISO 8601):<see cref="BuildInfo"/> 构建时写进去的,没设置就是空。</summary>
|
||
[JsonPropertyName("version")]
|
||
public string? Version { get; set; }
|
||
|
||
[JsonPropertyName("tag")]
|
||
public string? Tag { get; set; }
|
||
|
||
[JsonPropertyName("commit")]
|
||
public string? Commit { get; set; }
|
||
|
||
[JsonPropertyName("built_at")]
|
||
public string? BuiltAt { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 面板给节点的更新清单(<c>moshiro.update.offer</c>):<c>manifest</c> 是发布清单的原始文本,<c>signature</c> 是 base64 的签名。
|
||
/// 节点只是把它们原样放进数据目录;验签、拉取、重启都由宿主机上的更新器完成(节点进程不执行任何命令,也不信任清单的内容)。
|
||
/// </summary>
|
||
public sealed class UpdateOfferParams
|
||
{
|
||
[JsonPropertyName("manifest")]
|
||
public string Manifest { get; set; } = "";
|
||
|
||
[JsonPropertyName("signature")]
|
||
public string Signature { get; set; } = "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 面板给节点的定时重启设置(<c>moshiro.schedule</c> 通知):<c>restart_at</c> 形如 <c>04:30+08:00</c>,为空表示不定时重启。
|
||
/// 节点连上时补发、设置变化时群发;节点本地写了 <c>restart_at</c> 或环境变量 <c>MOSHIRO_RESTART_AT</c> 时以本地为准。
|
||
/// 节点只是到点自己有序退出,由服务管理器拉起,不执行任何命令。
|
||
/// </summary>
|
||
public sealed class ScheduleParams
|
||
{
|
||
[JsonPropertyName("restart_at")]
|
||
public string? RestartAt { get; set; }
|
||
}
|
||
|
||
/// <summary>宿主机更新器已经验证过的可用更新。</summary>
|
||
public sealed class UpdateAvailable
|
||
{
|
||
[JsonPropertyName("version")]
|
||
public string? Version { get; set; }
|
||
|
||
[JsonPropertyName("tag")]
|
||
public string? Tag { get; set; }
|
||
|
||
[JsonPropertyName("digest")]
|
||
public string? Digest { get; set; }
|
||
|
||
[JsonPropertyName("seq")]
|
||
public long Seq { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 宿主机更新器的状态(它写在 <c>update-status/status.json</c>,节点读出来随心跳上报):
|
||
/// <c>up_to_date</c>、<c>available</c>、<c>applying</c>、<c>failed</c>、<c>rolled_back</c>、<c>invalid</c>(清单验签或校验没过)。
|
||
/// 没有安装更新器的节点没有这个字段。
|
||
/// </summary>
|
||
public sealed class UpdateStatus
|
||
{
|
||
[JsonPropertyName("state")]
|
||
public string State { get; set; } = "";
|
||
|
||
/// <summary>正在运行的镜像 digest(更新器记录的;还没更新过时为空)。</summary>
|
||
[JsonPropertyName("current")]
|
||
public string? Current { get; set; }
|
||
|
||
[JsonPropertyName("available")]
|
||
public UpdateAvailable? Available { get; set; }
|
||
|
||
[JsonPropertyName("error")]
|
||
public string? Error { get; set; }
|
||
|
||
[JsonPropertyName("at")]
|
||
public long At { get; set; }
|
||
}
|
||
|
||
/// <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;
|
||
|
||
/// <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; }
|
||
|
||
/// <summary>日志只报计数(不带文本):自上次心跳以来各级别的条数、最多几个出现最多的类型,以及最新一条的编号;文本由 Backend 用 <c>moshiro.logs.fetch</c> 按需拉取。</summary>
|
||
[JsonPropertyName("log_summary")]
|
||
public LogSummaryDto? LogSummary { get; set; }
|
||
}
|
||
|
||
/// <summary>某一类日志(消息文本的稳定哈希)在一个级别上出现了多少次。</summary>
|
||
public sealed class LogTypeCountDto
|
||
{
|
||
[JsonPropertyName("type")]
|
||
public uint Type { get; set; }
|
||
|
||
[JsonPropertyName("level")]
|
||
public string Level { get; set; } = "info";
|
||
|
||
[JsonPropertyName("count")]
|
||
public long Count { get; set; }
|
||
}
|
||
|
||
public sealed class LogSummaryDto
|
||
{
|
||
[JsonPropertyName("last_id")]
|
||
public long LastId { get; set; }
|
||
|
||
[JsonPropertyName("info")]
|
||
public long Info { get; set; }
|
||
|
||
[JsonPropertyName("warn")]
|
||
public long Warn { get; set; }
|
||
|
||
[JsonPropertyName("error")]
|
||
public long Error { get; set; }
|
||
|
||
[JsonPropertyName("types")]
|
||
public List<LogTypeCountDto> Types { get; set; } = [];
|
||
}
|
||
|
||
/// <summary>Backend → 节点:拉取编号大于 <c>after_id</c> 的日志(最旧的在前),级别不低于 <c>min_level</c>。</summary>
|
||
public sealed class LogsFetchParams
|
||
{
|
||
[JsonPropertyName("after_id")]
|
||
public long AfterId { get; set; }
|
||
|
||
[JsonPropertyName("limit")]
|
||
public int Limit { get; set; } = 100;
|
||
|
||
[JsonPropertyName("min_level")]
|
||
public string? MinLevel { get; set; }
|
||
}
|
||
|
||
public sealed class LogEntryDto
|
||
{
|
||
[JsonPropertyName("id")]
|
||
public long Id { get; set; }
|
||
|
||
[JsonPropertyName("type")]
|
||
public uint Type { get; set; }
|
||
|
||
[JsonPropertyName("at_utc_ms")]
|
||
public long AtUtcMs { get; set; }
|
||
|
||
[JsonPropertyName("level")]
|
||
public string Level { get; set; } = "info";
|
||
|
||
[JsonPropertyName("message")]
|
||
public string Message { get; set; } = "";
|
||
|
||
[JsonPropertyName("fields")]
|
||
public Dictionary<string, string> Fields { get; set; } = [];
|
||
}
|
||
|
||
public sealed class LogsFetchResult
|
||
{
|
||
[JsonPropertyName("items")]
|
||
public List<LogEntryDto> Items { get; set; } = [];
|
||
|
||
/// <summary>节点上最新一条的编号:比 <c>after_id</c> 小说明节点重启过,编号从头开始。</summary>
|
||
[JsonPropertyName("last_id")]
|
||
public long LastId { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 拨号方到一个监听方(一个 IP 族)的线路健康。控制层(TCP+TLS 长连接,承载控制 RPC 和回传数据)与任务层(测量用的 TCP / QUIC)
|
||
/// 各有一个状态机:控制层断开才算两个节点失联,任务层只影响测量。
|
||
/// </summary>
|
||
public sealed class LinkHealth
|
||
{
|
||
[JsonPropertyName("listener_id")]
|
||
public string ListenerId { get; set; } = "";
|
||
|
||
[JsonPropertyName("ip_version")]
|
||
public string IpVersion { get; set; } = IpVersions.V4;
|
||
|
||
[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 ControlState State { get; set; } = ControlState.Dialing;
|
||
|
||
/// <summary>进入当前状态的时间(Unix 毫秒)。</summary>
|
||
[JsonPropertyName("since_ms")]
|
||
public long SinceMs { get; set; }
|
||
|
||
[JsonPropertyName("last_error")]
|
||
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_ping、connect、hold、tcp_transfer)。</summary>
|
||
[JsonPropertyName("tcp_blocks")]
|
||
public int TcpBlocks { get; set; }
|
||
|
||
[JsonPropertyName("quic")]
|
||
public QuicHealth Quic { get; set; } = new();
|
||
}
|
||
|
||
/// <summary>QUIC 数据通道(quic_ping、quic_stream 使用)的状态、握手统计和传输层统计。</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; }
|
||
|
||
[JsonPropertyName("retry_in_ms")]
|
||
public long? RetryInMs { get; set; }
|
||
|
||
[JsonPropertyName("handshake_attempts")]
|
||
public long HandshakeAttempts { get; set; }
|
||
|
||
[JsonPropertyName("handshake_successes")]
|
||
public long HandshakeSuccesses { get; set; }
|
||
|
||
[JsonPropertyName("handshake_failures")]
|
||
public long HandshakeFailures { get; set; }
|
||
|
||
[JsonPropertyName("handshake_timeouts")]
|
||
public long HandshakeTimeouts { get; set; }
|
||
|
||
[JsonPropertyName("handshake_p95_ms")]
|
||
public double? HandshakeP95Ms { get; set; }
|
||
|
||
[JsonPropertyName("handshake_sample_count")]
|
||
public long HandshakeSampleCount { get; set; }
|
||
|
||
/// <summary>QUIC packet-loss estimate from MsQuic's non-spurious loss counters; not Datagram application loss or retransmission.</summary>
|
||
[JsonPropertyName("transport_packet_loss_rate")]
|
||
public double? TransportPacketLossRate { get; set; }
|
||
|
||
[JsonPropertyName("transport_packets_sent")]
|
||
public ulong? TransportPacketsSent { get; set; }
|
||
|
||
[JsonPropertyName("transport_congestion_events")]
|
||
public uint? TransportCongestionEvents { get; set; }
|
||
|
||
[JsonPropertyName("transport_ecn_congestion_events")]
|
||
public uint? TransportEcnCongestionEvents { get; set; }
|
||
|
||
[JsonPropertyName("path_mtu")]
|
||
public ushort? PathMtu { get; set; }
|
||
}
|
||
|
||
/// <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("ready")]
|
||
Ready,
|
||
[JsonStringEnumMemberName("retrying")]
|
||
Retrying,
|
||
}
|
||
|
||
/// <summary>节点的 <c>fleet.hello</c> 附加字段:属主、系统与构建信息,以及(监听时)面向拨号方的接入信息。</summary>
|
||
public sealed class NodeHelloMeta
|
||
{
|
||
/// <summary>这个节点的属主:安装时固定在节点本地的 passkey 凭证 id。面板据此知道它绑定在哪个账户的哪把 passkey 上。</summary>
|
||
[JsonPropertyName("owners")]
|
||
public List<string>? Owners { get; set; }
|
||
|
||
[JsonPropertyName("ipv6")]
|
||
public Ipv6Diagnostic? Ipv6 { get; set; }
|
||
|
||
[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>节点作为监听方时,面向拨号方的接入信息。</summary>
|
||
public sealed class ListenInfo
|
||
{
|
||
[JsonPropertyName("tcp_port")]
|
||
public int TcpPort { get; set; }
|
||
|
||
[JsonPropertyName("quic_ports")]
|
||
public List<int> QuicPorts { get; set; } = [];
|
||
|
||
/// <summary>本机是否启用了 IPv6 监听;面板只给启用了的节点安排 IPv6 会话。</summary>
|
||
[JsonPropertyName("ipv6_supported")]
|
||
public bool Ipv6Supported { get; set; }
|
||
|
||
/// <summary>本机的公网地址(IPv4 公网地址、IPv6 全球单播地址):面板据此自动填 <c>public_host</c> 和 <c>ipv6_host</c>,管理员手填的优先。</summary>
|
||
[JsonPropertyName("addresses")]
|
||
public List<string>? Addresses { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 主机资源占用的只读快照(Agent → Backend,每隔几秒一次)。只有汇总的数字:<b>没有 CPU 型号、主机名、网卡名,也没有任何 IP 地址</b>。
|
||
/// 读不到的项是 null。
|
||
/// </summary>
|
||
public sealed class HostSnapshot
|
||
{
|
||
[JsonPropertyName("cpu_percent")]
|
||
public double? CpuPercent { get; set; }
|
||
|
||
[JsonPropertyName("mem_used")]
|
||
public long? MemUsed { get; set; }
|
||
|
||
[JsonPropertyName("mem_total")]
|
||
public long? MemTotal { get; set; }
|
||
|
||
[JsonPropertyName("disk_used")]
|
||
public long? DiskUsed { get; set; }
|
||
|
||
[JsonPropertyName("disk_total")]
|
||
public long? DiskTotal { get; set; }
|
||
|
||
/// <summary>所有物理网卡的累计收发字节(自开机以来)。</summary>
|
||
[JsonPropertyName("net_rx_total")]
|
||
public long? NetRxTotal { get; set; }
|
||
|
||
[JsonPropertyName("net_tx_total")]
|
||
public long? NetTxTotal { get; set; }
|
||
|
||
[JsonPropertyName("net_rx_bps")]
|
||
public long? NetRxBps { get; set; }
|
||
|
||
[JsonPropertyName("net_tx_bps")]
|
||
public long? NetTxBps { get; set; }
|
||
|
||
/// <summary>已统计接口的内核报告链路速率之和(Mbit/s);驱动不支持或无法读取时为空,不代表互联网带宽。</summary>
|
||
[JsonPropertyName("net_link_mbps")]
|
||
public long? NetLinkMbps { get; set; }
|
||
|
||
[JsonPropertyName("uptime_s")]
|
||
public long? UptimeSeconds { get; set; }
|
||
|
||
[JsonPropertyName("at_utc_ms")]
|
||
public long AtUtcMs { get; set; }
|
||
}
|
||
|
||
/// <summary>监听方眼中的一个已连接拨号方。</summary>
|
||
public sealed class ConnectedDialer
|
||
{
|
||
[JsonPropertyName("ip_version")]
|
||
public string IpVersion { get; set; } = IpVersions.V4;
|
||
|
||
[JsonPropertyName("id")]
|
||
public NodeId Id { get; set; }
|
||
|
||
[JsonPropertyName("version")]
|
||
public string? Version { get; set; }
|
||
|
||
/// <summary>拨号方的远端地址(经过 NAT 之后的公网 IP:端口)。</summary>
|
||
[JsonPropertyName("remote")]
|
||
public string? Remote { get; set; }
|
||
|
||
[JsonPropertyName("since_utc_ms")]
|
||
public long SinceUtcMs { get; set; }
|
||
}
|
||
|
||
/// <summary>监听方下发给拨号方的一条线路任务(期望状态类型 <c>line_task_set</c> 里的一项)。</summary>
|
||
public sealed class LineTask
|
||
{
|
||
[JsonPropertyName("id")]
|
||
public TaskId Id { get; set; }
|
||
|
||
[JsonPropertyName("kind")]
|
||
public string Kind { get; set; } = "";
|
||
|
||
[JsonPropertyName("params")]
|
||
public Dictionary<string, JsonElement> Params { get; set; } = [];
|
||
|
||
/// <summary>数据面用它区分任务;Agent 在本进程生命周期内分配。</summary>
|
||
[JsonPropertyName("stream_id")]
|
||
public uint StreamId { get; set; }
|
||
|
||
[JsonPropertyName("port")]
|
||
public int Port { get; set; }
|
||
}
|
||
|
||
/// <summary>这条线路的期望状态(监听方 → 拨号方):拨号方按它调和自己一侧的角色。</summary>
|
||
public sealed class LineTaskSet
|
||
{
|
||
public const string StateType = "line_task_set";
|
||
|
||
[JsonPropertyName("version")]
|
||
public long Version { get; set; }
|
||
|
||
[JsonPropertyName("tasks")]
|
||
public List<LineTask> Tasks { get; set; } = [];
|
||
|
||
/// <summary>监听方的数据面端口(拨号方把包发到这里)。</summary>
|
||
[JsonPropertyName("quic_ports")]
|
||
public List<int> QuicPorts { get; set; } = [];
|
||
|
||
[JsonPropertyName("tcp_ports")]
|
||
public List<int> TcpPorts { get; set; } = [];
|
||
}
|
||
|
||
/// <summary><c>moshiro.clock</c>:拨号方用它估计与监听方单调时钟的偏移(对称往返假设)。</summary>
|
||
public sealed class ClockParams
|
||
{
|
||
[JsonPropertyName("dialer_mono_us")]
|
||
public long DialerMonoUs { get; set; }
|
||
}
|
||
|
||
public sealed class ClockResult
|
||
{
|
||
[JsonPropertyName("listener_mono_us")]
|
||
public long ListenerMonoUs { get; set; }
|
||
}
|
||
|
||
/// <summary>按需测试的一个阶段(监听方 → 拨号方):监听方编排时间,拨号方准备好自己一侧的角色之后应答。</summary>
|
||
public sealed class TestPhaseStartParams
|
||
{
|
||
[JsonPropertyName("test_id")]
|
||
public TestId TestId { get; set; }
|
||
|
||
[JsonPropertyName("phase")]
|
||
public int Phase { get; set; }
|
||
|
||
[JsonPropertyName("kind")]
|
||
public string Kind { get; set; } = "";
|
||
|
||
[JsonPropertyName("params")]
|
||
public Dictionary<string, JsonElement> Params { get; set; } = [];
|
||
|
||
[JsonPropertyName("stream_id")]
|
||
public uint StreamId { get; set; }
|
||
|
||
[JsonPropertyName("duration_s")]
|
||
public int DurationSeconds { get; set; }
|
||
|
||
[JsonPropertyName("port")]
|
||
public int Port { get; set; }
|
||
}
|
||
|
||
public sealed class TestPhaseStartResult
|
||
{
|
||
[JsonPropertyName("accepted")]
|
||
public bool Accepted { get; set; }
|
||
}
|
||
|
||
/// <summary>阶段结束(监听方 → 拨号方):拨号方收尾并把自己一侧的列块交给监听方。<c>sent_count</c> 是监听方一侧发出的包数(单向数据流用)。</summary>
|
||
public sealed class TestPhaseEndParams
|
||
{
|
||
[JsonPropertyName("test_id")]
|
||
public TestId TestId { get; set; }
|
||
|
||
[JsonPropertyName("phase")]
|
||
public int Phase { get; set; }
|
||
|
||
[JsonPropertyName("stream_id")]
|
||
public uint StreamId { get; set; }
|
||
|
||
[JsonPropertyName("sent_count")]
|
||
public long SentCount { get; set; }
|
||
}
|
||
|
||
/// <summary>拨号方收尾之后回复:它交给监听方的列块个数(监听方据此等待全部列块到齐再进入下一阶段)。</summary>
|
||
public sealed class TestPhaseEndResult
|
||
{
|
||
[JsonPropertyName("fragments")]
|
||
public int Fragments { get; set; }
|
||
}
|
||
|
||
public sealed class TestAbortParams
|
||
{
|
||
[JsonPropertyName("test_id")]
|
||
public TestId TestId { get; set; }
|
||
}
|
||
|
||
/// <summary>拨号方一侧的列块放在 RawChunk 头部 params 里的保留键:本块覆盖的序号范围 [from, to)(单向数据流由监听方一侧按它取行)。</summary>
|
||
public static class FragmentKeys
|
||
{
|
||
public const string SeqFrom = "_seq_from";
|
||
public const string SeqTo = "_seq_to";
|
||
}
|
||
|
||
[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||
[JsonSerializable(typeof(LineTaskSet))]
|
||
[JsonSerializable(typeof(ClockParams))]
|
||
[JsonSerializable(typeof(ClockResult))]
|
||
[JsonSerializable(typeof(TestPhaseStartParams))]
|
||
[JsonSerializable(typeof(TestPhaseStartResult))]
|
||
[JsonSerializable(typeof(TestPhaseEndParams))]
|
||
[JsonSerializable(typeof(TestPhaseEndResult))]
|
||
[JsonSerializable(typeof(TestAbortParams))]
|
||
[JsonSerializable(typeof(TaskSet))]
|
||
[JsonSerializable(typeof(TestRunParams))]
|
||
[JsonSerializable(typeof(TestRunResult))]
|
||
[JsonSerializable(typeof(TestCancelParams))]
|
||
[JsonSerializable(typeof(DialerDisconnectParams))]
|
||
[JsonSerializable(typeof(LinkRetryParams))]
|
||
[JsonSerializable(typeof(HostSnapshot))]
|
||
[JsonSerializable(typeof(TestStatusParams))]
|
||
[JsonSerializable(typeof(AgentEventParams))]
|
||
[JsonSerializable(typeof(DialTargets))]
|
||
[JsonSerializable(typeof(UpdateOfferParams))]
|
||
[JsonSerializable(typeof(ScheduleParams))]
|
||
[JsonSerializable(typeof(LogsFetchParams))]
|
||
[JsonSerializable(typeof(LogsFetchResult))]
|
||
[JsonSerializable(typeof(LogSummaryDto))]
|
||
[JsonSerializable(typeof(UpdateStatus))]
|
||
[JsonSerializable(typeof(NodeHeartbeatExtra))]
|
||
[JsonSerializable(typeof(NodeHelloMeta))]
|
||
[JsonSerializable(typeof(NodeEnrollMeta))]
|
||
[JsonSerializable(typeof(PlatformInfo))]
|
||
[JsonSerializable(typeof(Ipv6Diagnostic))]
|
||
[JsonSerializable(typeof(RawChunkHeader))]
|
||
public sealed partial class MoShiroJsonContext : JsonSerializerContext;
|