# Fleet:身份、注册与连接 ## 1. 概览 ```text fleet-enroll(一次性运行) │ ① 用加入令牌注册,生成身份 ▼ ┌────────── 节点 ──────────┐ ┌──────────── 服务端 ────────────┐ │ Fleet.Hosting │ ② wss 连接 │ Fleet.Server.Link │ │ └─ Fleet.Link ─────────────── 签名认证 ────>│ 连接注册表 · 在线事件 │ │ ├─ Rpc 路由 │<── state.sync ──│ 期望状态提供者(产品实现) │ │ ├─ 期望状态调和 │── outbox.push ─>│ 收件箱(产品实现,持久化后 ACK) │ │ └─ 可靠发件箱 │<── outbox.ack ──│ │ │ │ │ Fleet.Server.Enrollment │ │ Fleet.Link(点对点)──────── TLS + 签名 ────> 另一个节点(例如 Probe → Agent) │ └──────────────────────────┘ └────────────────────────────────┘ ``` ## 2. 身份(Fleet.Identity) | 对象 | 算法 | 保存位置 | | --- | --- | --- | | 节点身份密钥 | ECDSA P-256(BCL 自带;Ed25519 不在 BCL 中) | `/node.key`(0600);服务端只保存公钥 | | 点对点 TLS 证书 | 自签名,ECDSA P-256 | `/tls.pfx`;对端按 SPKI SHA-256 指纹校验 | | 签名串 | `"fleet|v1||" + 各字段` | 带用途前缀,防止同一个签名在不同场景下被复用 | 用途前缀的取值:`auth`(连接服务端)、`peer`(点对点)、`enroll`(注册时的持有证明)。产品可以追加自己的用途(例如 `myapp.xxx`),但不能占用 `fleet` 保留的用途。 ## 3. 注册(Fleet.Enrollment) ```text 管理员 ── 创建加入令牌 {labels, max_uses, expires_at, auto_approve, product_meta?} ──> 服务端 <── 令牌明文(只返回一次)+ 安装命令 节点:fleet-enroll --server https://panel --join jt_xxx --name jp-1 --identity-dir /etc/myapp ├─ 生成身份密钥和 TLS 证书 ├─ POST │ { join_token, name, pubkey, tls_spki, proof = 签名("enroll", join_token 哈希 + pubkey), │ node_version, meta(产品自定义的 JSON,经产品的 JsonTypeInfo 校验)} │ 服务端:校验令牌 → uses+1 → 创建节点(active 或 pending)→ 触发 OnEnrolled 事件(产品挂钩) └─ 写入 /node.json { node_id, server, … },然后退出 ``` - **名称冲突**:同名节点已存在时,新注册进入 `pending`,由管理员选择“接管”(旧记录换成新公钥,历史数据保留)或“作为新节点”。 - **节点状态**:`pending`(能连接,但不下发状态、不接收上报)、`active`、`disabled`(拒绝连接)。 - 注册逻辑只存在于 `fleet-enroll` 中,**常驻的业务进程不包含注册代码,也不使用 HttpClient**。 - 产品可以复用 `fleet-enroll` 本身,也可以把 `Fleet.Enrollment.Client` 嵌入自己的安装工具中。 ## 4. RPC(Fleet.Rpc) - 信封采用 JSON-RPC 2.0;方法名为 `领域.动作`。`fleet.*` 保留给框架,产品使用自己的前缀,例如 `monitor.*`。 - 二进制帧:`u8 channel | 正文`。channel `0` 到 `15` 保留给框架,产品从 `16` 开始注册自己的二进制通道。 - 错误:`error.data.code` 为 snake_case 字符串,框架的错误码以 `fleet_` 开头,产品可以扩展。 - **与传输方式无关**:同一套路由既可以跑在 wss 上,也可以跑在 TLS 帧(`u32 长度 | u8 类型 | 正文`)上。 框架保留的方法: | 方法 | 方向 | 说明 | | --- | --- | --- | | `fleet.challenge` / `fleet.auth` | 服务端→节点 / 节点→服务端 | 签名认证(`auth` 用途,nonce 30 秒内有效) | | `fleet.hello` | 节点→服务端 | 节点版本、能力列表(框架 + 产品)、本地策略摘要、当前时间 → 返回时钟偏差 | | `fleet.heartbeat` | 节点→服务端 | 每 10 s 一次;产品可以附加进程级计数 | | `fleet.state.sync` | 服务端→节点 | `{version, doc}` → `{applied_version, rejected[]}` | | `fleet.outbox.ack` | 服务端→节点 | `{ids[], duplicates[]}` | | `fleet.revoked` | 服务端→节点 | 节点被禁用或接管,随后断开 | ## 5. 连接生命周期(Fleet.Link) ```text connect(wss) → fleet.challenge → fleet.auth → fleet.hello → 状态同步 → 正常运行 ▲ │ └──── 指数退避重连(1 s → 60 s,带随机抖动;401 或 revoked 时停止重试)◀─ 断开 ``` - 能力协商:双方交换 capabilities;调用对方不支持的方法时,在本地直接返回 `fleet_unsupported`,不会发出请求。 - 节点本地策略:`allow_methods`(白名单,默认只包含本地已注册的方法)、`max_frame_bytes`、`max_inflight`。 ## 6. 可靠发件箱(Outbox) ```text 产品 → outbox.EnqueueAsync(msg) → 有界队列(按字节和条数限制;满时按策略:DropOldest 或 Reject) → 发送(二进制通道或 JSON)→ 服务端收件箱处理并持久化 → fleet.outbox.ack → 从队列移除 ``` - 投递语义为**至少一次**:每条消息都带有 `msg_id`(UUIDv7),服务端按此去重。 - 在途数据按字节限制(默认 4 MiB),保证服务端慢时不会把内存撑爆。 - 丢弃消息时会累计缺口计数,通过 `OnGap` 回调通知产品,由产品决定如何上报。 - 默认只保存在内存中,不落盘。产品如果需要持久化,可以自行实现 `IOutboxStore`(Monitor 不使用)。 ## 7. 期望状态同步(State Sync) ```text 服务端:IStateProvider.GetAsync(node) → {version, doc} 发生变化时调用 NotifyChanged(node) → 推送 fleet.state.sync 节点: IStateReconciler.ApplyAsync(doc) → {applied_version, rejected[]} 重连后,服务端总会重新推送当前版本 ``` - 下发的是**完整快照**而不是命令,所以天然幂等,断线重连后也能自动收敛。 - `rejected[]` 为结构化原因,例如 `unsupported`、`budget_exceeded`、`policy_denied`,服务端可以把它展示在面板上。 ## 8. 节点间点对点会话 ```text 节点 A(发起方,例如 Probe)── TLS(按 SPKI 校验证书)──> 节点 B(监听方,例如 Agent) <── fleet.challenge ── ── fleet.auth(peer 用途,节点 A 的私钥签名)──> 节点 B 用服务端下发的对端公钥列表验签(通常经 state.sync 下发) 之后使用同一套 Fleet.Rpc:方法路由、二进制通道、发件箱都可以复用 ``` - 对端的公钥和允许连接的节点列表,由产品通过期望状态下发给监听方。Fleet 只负责认证和会话,拓扑由产品决定。 - 数据面的 UDP、原始 TCP 流等通道由产品自己实现。Fleet 只提供会话密钥派生(`session_key`)和首帧 MAC 的辅助函数。 ## 9. 宿主(Fleet.Hosting) - `ModuleHost`:按依赖顺序启动模块,按相反顺序停止;隔离故障,重启时退避。 - `NodeConfig`:源生成 JSON 读取,支持环境变量覆盖,启动时校验。 - `ConfigStore`:业务进程唯一允许写文件的地方,只能写配置目录。 - `ServiceShell`:处理 SIGTERM 和 Ctrl+C,支持 systemd `sd_notify`、OpenWrt procd(前台运行,由 procd 负责重启)和 Windows 服务。 - `HealthBeacon`:进程完成启动并与服务端握手后,写出 `{app, version, pid, ok_at}` 到本地信标文件或命名管道,供 `fleet-updater` 确认新版本是否健康。 ## 10. 服务端存储抽象 ```csharp public interface IFleetStore { ValueTask FindJoinTokenAsync(ReadOnlyMemory tokenHash, CancellationToken ct); ValueTask> CreateNodeAsync(NewNode node, CancellationToken ct); ValueTask GetNodeAsync(NodeId id, CancellationToken ct); ValueTask UpdatePresenceAsync(NodeId id, Presence presence, CancellationToken ct); // 令牌、审批、接管、禁用等 } ``` 产品可以使用 `Fleet.Server.Sqlite`,也可以把这些表放进自己的数据库中。Monitor 的做法是把它们放进自己的 `monitor.db`,并由同一个单写入者负责写入。