Files
MoShiro/docs/fleet/link.md
T
2026-10-04 21:46:39 +08:00

138 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 中) | `<identity_dir>/node.key`(0600);服务端只保存公钥 |
| 点对点 TLS 证书 | 自签名,ECDSA P-256 | `<identity_dir>/tls.pfx`;对端按 SPKI SHA-256 指纹校验 |
| 签名串 | `"fleet|v1|<purpose>|" + 各字段` | 带用途前缀,防止同一个签名在不同场景下被复用 |
用途前缀的取值:`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 <enroll 端点>
│ { join_token, name, pubkey, tls_spki, proof = 签名("enroll", join_token 哈希 + pubkey),
│ node_version, meta(产品自定义的 JSON,经产品的 JsonTypeInfo 校验)}
│ 服务端:校验令牌 → uses+1 → 创建节点(active 或 pending)→ 触发 OnEnrolled 事件(产品挂钩)
└─ 写入 <identity_dir>/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<TDoc>.GetAsync(node) → {version, doc}
发生变化时调用 NotifyChanged(node) → 推送 fleet.state.sync
节点: IStateReconciler<TDoc>.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<T>`:源生成 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<JoinToken?> FindJoinTokenAsync(ReadOnlyMemory<byte> tokenHash, CancellationToken ct);
ValueTask<Result<NodeRecord>> CreateNodeAsync(NewNode node, CancellationToken ct);
ValueTask<NodeRecord?> GetNodeAsync(NodeId id, CancellationToken ct);
ValueTask UpdatePresenceAsync(NodeId id, Presence presence, CancellationToken ct);
// 令牌、审批、接管、禁用等
}
```
产品可以使用 `Fleet.Server.Sqlite`,也可以把这些表放进自己的数据库中。Monitor 的做法是把它们放进自己的 `monitor.db`,并由同一个单写入者负责写入。