102 lines
8.7 KiB
Markdown
102 lines
8.7 KiB
Markdown
# Fleet:通用节点框架
|
||
|
||
「很多节点连到一个面板」这类产品都要解决同一批问题:节点怎么接入、怎么证明身份、怎么保持连接、怎么可靠上报、怎么接收配置。Fleet 把这些做成与业务无关的库,MoShiro 是它的第一个使用者;产品只写业务。Fleet 不引用任何 `MoShiro.*`,也不出现产品的名字、端口和协议前缀,由 `ArchitectureTests` 在构建时检查。
|
||
|
||
```text
|
||
┌──────────────────── 产品层(MoShiro:Protocol · Measurement · Agent · Probe · Backend)────────────────────┐
|
||
└────────────────────────────────────────────┬─────────────────────────────────────────────────────────────┘
|
||
│ 只依赖 Fleet
|
||
┌───────────────────────────── Fleet ────────┴─────────────────────────────────────────────────────────────┐
|
||
│ 节点侧(只依赖 BCL,NativeAOT) 服务端侧(ASP.NET Core) │
|
||
│ Fleet.Core Result、退避、有界队列、ID、令牌哈希 Fleet.Server.Abstractions IFleetStore 等接口 │
|
||
│ Fleet.Identity 密钥、签名、证书、KeyStore Fleet.Server.Enrollment 加入令牌、注册、审批 │
|
||
│ Fleet.Rpc JSON-RPC 信封、路由、二进制帧 Fleet.Server.Link wss 网关、在线表、状态下发 │
|
||
│ Fleet.Link 会话、认证、重连、发件箱、状态同步、点对点 │
|
||
│ Fleet.Hosting 模块宿主、配置、日志、服务外壳 工具:fleet-enroll(一次性注册) │
|
||
│ Fleet.Enrollment.Client 注册流程(给 fleet-enroll 用) │
|
||
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 设计原则
|
||
|
||
1. **与业务无关**:产品通过扩展点接入:元数据类型、RPC 方法、发件箱消息类型、期望状态文档类型。
|
||
2. **安全默认值**:Fleet 永远不提供 exec、shell、文件读写、远程下发代码这类方法;节点只响应本地注册过的方法(本地策略 `allow_methods` 还能收窄);调用对方不支持的方法在本地直接返回 `fleet_unsupported`。
|
||
3. **权限分离**:注册(`fleet-enroll`,一次性)和常驻业务进程是两个进程;常驻进程不含注册代码,也不使用 `HttpClient`。
|
||
4. **NativeAOT**:节点侧只依赖 BCL,泛型扩展点通过产品传入的 `JsonTypeInfo<T>` 序列化,不使用反射。
|
||
5. **更新不在 Fleet 里**:节点进程没有自更新能力。容器节点的签名更新由宿主机上的更新器完成,见 [`docs/deployment.md`](../../docs/deployment.md);纯二进制节点的更新是待办,见 [`TODO.md`](../../TODO.md)。
|
||
|
||
## 身份
|
||
|
||
| 对象 | 算法 | 保存位置 |
|
||
| --- | --- | --- |
|
||
| 节点身份密钥 | ECDSA P-256(BCL 自带) | `<identity_dir>/node.key`(0600);服务端只保存公钥 |
|
||
| 点对点 TLS 证书 | 自签名 ECDSA P-256 | `<identity_dir>/tls.pfx`;对端按 SPKI SHA-256 指纹校验 |
|
||
| 签名串 | `"fleet|v1|<purpose>|" + 各字段` | 带用途前缀,防止同一个签名在不同场景被复用 |
|
||
|
||
用途:`auth`(连接服务端)、`peer`(点对点)、`enroll`(注册时的持有证明)。产品可以追加自己的用途,不能占用 `fleet` 保留的。
|
||
|
||
## 注册
|
||
|
||
```text
|
||
管理员 ── 创建加入令牌 {labels, max_uses, expires_at, auto_approve} ──> 服务端
|
||
<── 令牌明文(只返回一次)+ 安装命令
|
||
|
||
节点: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", 令牌哈希 + pubkey), node_version, meta }
|
||
│ 服务端:校验令牌 → uses+1 → 创建节点(active 或 pending)→ 触发产品的 IEnrollmentHook
|
||
└─ 写入 <identity_dir>/node.json { node_id, server, … },然后退出
|
||
```
|
||
|
||
- 同名节点已存在时,新注册进入 `pending`,管理员选择「接管」(旧记录换成新公钥,历史数据保留)或「作为新节点」。
|
||
- 节点状态:`pending`(能连接,但不下发状态、不接收上报)、`active`、`disabled`(拒绝连接)。
|
||
|
||
## RPC
|
||
|
||
- JSON-RPC 2.0;方法名 `领域.动作`,`fleet.*` 保留给框架,产品用自己的前缀(MoShiro 是 `moshiro.*`)。
|
||
- 二进制帧 `u8 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 秒一次,产品可以附加计数 |
|
||
| `fleet.state.sync` | 服务端→节点 | `{version, doc}` → `{applied_version, rejected[]}` |
|
||
| `fleet.outbox.ack` | 服务端→节点 | `{ids[], duplicates[]}` |
|
||
| `fleet.revoked` | 服务端→节点 | 节点被禁用或接管,随后断开 |
|
||
|
||
## 连接
|
||
|
||
```text
|
||
connect(wss) → fleet.challenge → fleet.auth → fleet.hello → 状态同步 → 正常运行
|
||
▲ │
|
||
└──── 指数退避重连(1 s → 60 s,带抖动;401 或 revoked 时停止重试)◀─ 断开
|
||
```
|
||
|
||
本地策略:`allow_methods`、`max_frame_bytes`、`max_inflight`。
|
||
|
||
**可靠发件箱**:产品 `EnqueueAsync` → 有界队列(按字节和条数;满时 `DropOldest` 或 `Reject`)→ 发送 → 服务端收件箱(`IInbox`)持久化后 `fleet.outbox.ack` → 移除。至少一次,按 `msg_id`(UUIDv7)去重;在途数据默认限 4 MiB;丢弃会累计缺口,通过 `OnGap` 通知产品。只在内存里,不落盘。
|
||
|
||
**期望状态同步**:服务端 `IStateProvider<TDoc>` 给每个节点算 `{version, doc}`,变化时 `NotifyChanged` 推送;节点 `StateHandler<TDoc>` 调和后返回 `{applied_version, rejected[]}`。下发的是**完整快照**,天然幂等,重连后服务端总会重发当前版本。`rejected[]` 带结构化原因(`unsupported`、`budget_exceeded`、`policy_denied`),面板可以直接展示。
|
||
|
||
## 点对点会话
|
||
|
||
```text
|
||
节点 A(发起方,如 Probe)── TLS(按 SPKI 校验证书)──> 节点 B(监听方,如 Agent)
|
||
<── fleet.challenge ──
|
||
── fleet.auth(peer 用途,A 的私钥签名)──>
|
||
B 用服务端下发的对端公钥列表验签(经 state.sync 下发),之后复用同一套 Rpc:路由、二进制通道、发件箱
|
||
```
|
||
|
||
对端公钥和允许连接的节点由产品通过期望状态下发给监听方,Fleet 只管认证和会话,拓扑由产品决定;UDP、原始 TCP 数据面由产品自己实现,Fleet 只提供会话密钥派生和首帧 MAC 的辅助函数。
|
||
|
||
## 宿主与服务端抽象
|
||
|
||
- `ModuleHost`:按依赖顺序启动模块、相反顺序停止,隔离故障,重启时退避。
|
||
- `NodeConfig`:源生成 JSON 读取,环境变量覆盖,启动时校验。
|
||
- `ConfigStore`:常驻进程里**唯一**允许写文件的地方,只能写配置目录,写入原子替换、权限 0600;父目录不存在会创建。
|
||
- `ServiceShell`:SIGTERM、Ctrl+C,systemd `sd_notify`、OpenWrt procd(前台运行)和 Windows 服务。
|
||
- 服务端接口:`IFleetStore`(节点、令牌、公钥、状态版本)、`IStateProvider<TDoc>`、`IInbox`、`INodeEventSink`、`IEnrollmentHook`。MoShiro 把 `IFleetStore` 放进自己的 `moshiro.db`,和业务数据由同一个单写入者写入。
|