Files
MoShiro/docs/fleet/link.md
T
justme 2e146d4da7
ci/woodpecker/push/main-amd64-build Pipeline was successful
refactor!: rename the project from Monitor to MoShiro [CI]
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
2026-10-07 02:03:25 +08:00

8.6 KiB
Raw Blame History

Fleet:身份、注册与连接

1. 概览

             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

用途前缀的取值:auth(连接服务端)、peer(点对点)、enroll(注册时的持有证明)。产品可以追加自己的用途(例如 myapp.xxx),但不能占用 fleet 保留的用途。

3. 注册(Fleet.Enrollment)

管理员 ── 创建加入令牌 {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.* 保留给框架,产品使用自己的前缀,例如 moshiro.*。
  • 二进制帧: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 服务端→节点 节点被禁用或接管,随后断开
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)

产品 → outbox.EnqueueAsync(msg) → 有界队列(按字节和条数限制;满时按策略:DropOldest 或 Reject)
     → 发送(二进制通道或 JSON)→ 服务端收件箱处理并持久化 → fleet.outbox.ack → 从队列移除
  • 投递语义为至少一次:每条消息都带有 msg_id(UUIDv7),服务端按此去重。
  • 在途数据按字节限制(默认 4 MiB),保证服务端慢时不会把内存撑爆。
  • 丢弃消息时会累计缺口计数,通过 OnGap 回调通知产品,由产品决定如何上报。
  • 默认只保存在内存中,不落盘。产品如果需要持久化,可以自行实现 IOutboxStore(MoShiro 不使用)。

7. 期望状态同步(State Sync)

服务端: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. 节点间点对点会话

节点 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. 服务端存储抽象

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,也可以把这些表放进自己的数据库中。MoShiro 的做法是把它们放进自己的 moshiro.db,并由同一个单写入者负责写入。