Files
MoShiro/backend/fleet/src/Fleet.Rpc/FleetMethods.cs
T
justme a2f88a344b
ci/woodpecker/tag/gitea-release Pipeline is pending
ci/woodpecker/tag/linux-release Pipeline failed
ci/woodpecker/tag/windows-release Pipeline was canceled
feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
Join codes need no passkey touch and carry only public values (genesis
fingerprint, owner credentials, installer hashes). Nodes pin the genesis and
owners at enrollment and only follow passkey-signed operator set versions.
The panel's server key is listed in the set; pinned nodes require its
signature and run the control link under AES-256-GCM. Password sessions carry
no capabilities. The updater adopts a next_key from a signed manifest.

Bump version to 0.2.0. [CI SKIP]
2026-10-08 03:21:44 +08:00

178 lines
6.0 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
namespace Fleet.Rpc;
/// <summary>框架保留的方法名。<c>fleet.*</c> 只属于框架,产品使用自己的前缀。</summary>
public static class FleetMethods
{
public const string Challenge = "fleet.challenge";
public const string Auth = "fleet.auth";
public const string Hello = "fleet.hello";
public const string Heartbeat = "fleet.heartbeat";
public const string StateSync = "fleet.state.sync";
public const string OutboxAck = "fleet.outbox.ack";
public const string Revoked = "fleet.revoked";
}
/// <summary>
/// 框架自己的方法声明,两个平面各一份。认证和握手阶段由连接代码直接处理的(<c>Inline</c>)不经过路由器。
/// 同一个方法名在两个平面里可以有不同的发起方:<c>fleet.state.sync</c> 在节点面是服务端发给节点,在对等面是 Agent 发给 Probe。
/// </summary>
public static class FleetMethodTable
{
public static readonly MethodTable Table = new(
[
// 节点面
MethodSpec.Notification(FleetMethods.Challenge, RpcSide.Server).AsInline(),
MethodSpec.Request(FleetMethods.Auth, RpcSide.Node).AsInline(),
MethodSpec.Request(FleetMethods.Hello, RpcSide.Node).AsInline(),
MethodSpec.Notification(FleetMethods.Heartbeat, RpcSide.Node).AsInline(),
MethodSpec.Request(FleetMethods.StateSync, RpcSide.Server),
MethodSpec.Notification(FleetMethods.OutboxAck, RpcSide.Server),
MethodSpec.Notification(FleetMethods.Revoked, RpcSide.Server),
// 对等面
MethodSpec.Notification(PeerMethods.Challenge, RpcSide.Listener).AsInline(),
MethodSpec.Request(PeerMethods.Auth, RpcSide.Dialer).AsInline(),
MethodSpec.Request(FleetMethods.StateSync, RpcSide.Listener),
MethodSpec.Notification(FleetMethods.OutboxAck, RpcSide.Listener),
]);
}
/// <summary>二进制通道:0–15 保留给框架,产品从 16 开始。</summary>
public static class FleetChannels
{
public const byte FirstProductChannel = 16;
}
public sealed class ChallengeParams
{
[JsonPropertyName("nonce")]
public string Nonce { get; set; } = "";
[JsonPropertyName("expires_at_ms")]
public long ExpiresAtMs { get; set; }
/// <summary>服务器这次连接的临时 ECDH 公钥(SPKI,base64);服务器有身份密钥时才有。</summary>
[JsonPropertyName("server_eph")]
public string? ServerEphemeral { get; set; }
/// <summary>服务器密钥对「随机数加临时公钥」的签名(base64):节点据此确认对面是操作员授权的服务器。</summary>
[JsonPropertyName("server_sig")]
public string? ServerSignature { get; set; }
}
public sealed class AuthParams
{
[JsonPropertyName("node_id")]
public string NodeId { get; set; } = "";
/// <summary>对 <c>fleet|v1|auth|{node_id}|{nonce}</c> 的签名(base64)。</summary>
[JsonPropertyName("signature")]
public string Signature { get; set; } = "";
/// <summary>节点的临时 ECDH 公钥(SPKI,base64):有它就是安全链路,签名覆盖双方的临时公钥,认证结果起所有帧都是加密的。</summary>
[JsonPropertyName("node_eph")]
public string? NodeEphemeral { get; set; }
}
public sealed class AuthResult
{
/// <summary>pending | active</summary>
[JsonPropertyName("status")]
public string Status { get; set; } = "";
}
public sealed class HelloParams
{
[JsonPropertyName("node_version")]
public string NodeVersion { get; set; } = "";
[JsonPropertyName("capabilities")]
public List<string> Capabilities { get; set; } = [];
[JsonPropertyName("now_utc_ms")]
public long NowUtcMs { get; set; }
/// <summary>产品自定义的元数据(例如 MoShiro 的 tls_spki、probe_ports、local_limits 摘要)。</summary>
[JsonPropertyName("meta")]
public JsonElement? Meta { get; set; }
}
public sealed class HelloResult
{
[JsonPropertyName("server_time_ms")]
public long ServerTimeMs { get; set; }
/// <summary>节点时钟 − 服务端时钟(毫秒,未扣除网络往返)。</summary>
[JsonPropertyName("clock_offset_ms")]
public long ClockOffsetMs { get; set; }
}
public sealed class HeartbeatParams
{
[JsonPropertyName("extra")]
public JsonElement? Extra { get; set; }
}
public sealed class StateSyncParams
{
[JsonPropertyName("type")]
public string Type { get; set; } = "";
[JsonPropertyName("version")]
public long Version { get; set; }
[JsonPropertyName("doc")]
public JsonElement Doc { get; set; }
}
public sealed class StateSyncResult
{
[JsonPropertyName("applied_version")]
public long AppliedVersion { get; set; }
[JsonPropertyName("rejected")]
public List<RejectedItem> Rejected { get; set; } = [];
}
public sealed class RejectedItem
{
[JsonPropertyName("id")]
public string Id { get; set; } = "";
/// <summary>unsupported | budget_exceeded | policy_denied | …</summary>
[JsonPropertyName("reason")]
public string Reason { get; set; } = "";
}
public sealed class OutboxAckParams
{
[JsonPropertyName("ids")]
public List<string> Ids { get; set; } = [];
[JsonPropertyName("duplicates")]
public List<string> Duplicates { get; set; } = [];
}
public sealed class RevokedParams
{
[JsonPropertyName("reason")]
public string Reason { get; set; } = "";
}
[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(RpcMessage))]
[JsonSerializable(typeof(ChallengeParams))]
[JsonSerializable(typeof(AuthParams))]
[JsonSerializable(typeof(AuthResult))]
[JsonSerializable(typeof(HelloParams))]
[JsonSerializable(typeof(HelloResult))]
[JsonSerializable(typeof(HeartbeatParams))]
[JsonSerializable(typeof(StateSyncParams))]
[JsonSerializable(typeof(StateSyncResult))]
[JsonSerializable(typeof(OutboxAckParams))]
[JsonSerializable(typeof(RevokedParams))]
public sealed partial class FleetRpcContext : JsonSerializerContext;