对应审查建议的 P0 两条:这两项是**真实可利用的攻击路径**,其余(发现最小化/失败封禁等) 留待后续批次。 ## ① 证书指纹钉住(TOFU)—— 挡同网段中间人 自签证书没有 CA 链,历史上客户端只能 `insecure` 接受任意证书 → 攻击者可**透明中继**: 拿不到钥匙(挑战-应答挡得住冒名),但能读到截屏/剪贴板/键入/exec 输出。 - 被控端:`install.ps1` 打印证书指纹(sha256 of DER)供**带外核对**; `system.info.build.cert_sha256` 与 `auth_ok.build.cert_sha256` 上报 - 控制端:首次连接自动钉进 `nodes.<名>.cert_sha256`;之后**严格比对**, 不一致即 `E_CERT_MISMATCH`(-32422) 拒连,并提示 `--forget-cert` 重钉 - 额外做**交叉确认**:实际握手证书 vs auth_ok 自报,不一致同样拒绝(防自报被篡改) - 指纹基于 **DER**(不是 PEM 字节),保证与 Windows .NET `GetRawCertData()` 同值 **踩坑记录(写进代码注释,避免后人重走)**: 1. `ws` 客户端的 `upgrade` 事件**只传 IncomingMessage**,拿不到 socket 2. socket 要在 `open` 之后从 `ws._socket` 取(TLSSocket) 3. Node ≥15 的 `getPeerCertificate()` 返回**对象**,DER 在 `.raw` 上(老版本才是 Buffer) —— 现已双兼容 ## ② 来源网段白名单 `allow_from` —— 收敛暴露面 被控端默认监听 `0.0.0.0`(远程接入所需),但**换到不可信网络就自动暴露**。 - 支持 CIDR / 单 IP / `*`;**未配置 = 放行全部**(保持向后兼容,不打断现有部署) - 拒绝发生在**业务握手之前**(不做 TLS/挑战应答,省资源),并记 `net.deny` 审计 - 判定按**前缀比特**而非字符串相等:Node 在 IPv6 监听上把 IPv4 客户端报成 `::ffff:a.b.c.d`,已归一化(单测覆盖) - `system.info.network.allow_from` 上报状态;`nodeagent info` 一眼看出是否已收敛 ## 验证(本地真跑,非纸面) - 白名单不含本机 → 连接被拒(agent 侧 WARN + 审计留痕)✓ - 白名单含本机 → 连上,并**自动钉住**指纹 ✓ - **换掉被控端证书后重连 → `E_CERT_MISMATCH` 拒绝**(期望 2ed4957b… / 实际 ca8dc20c…)✓ - 新增 e2e 用例(自起 TLS agent)覆盖上述四种情形 + agent 自报与实际证书一致性 ✓ - 新增单测 9 例(CIDR 前缀匹配 / IPv4-mapped 归一 / IPv6 zone / PEM≡DER / 篡改即变) - 全量:单测 **159**(150→159)· e2e **29**(28→29)全绿 ## 文档 新增 `docs/SECURITY.md`:信任模型(钥匙=root)、认证现状与**已知限制**(ACL 身份维度仅在 ed25519 有效)、本次两项加固的用法、尚未收敛的 6 项、应急处置表。
110 lines
6.0 KiB
Markdown
110 lines
6.0 KiB
Markdown
# 版本与兼容性契约
|
||
|
||
> 为什么需要这份文档:agent 与 CLI/MCP 是**分开更新的**(CLI 随控制端装,agent 推/拉到被控端),
|
||
> 新旧混用是常态而非例外。没有明文契约,升级顺序、什么算破坏性改动、出问题怎么定位全靠猜。
|
||
|
||
## 1. 版本号
|
||
|
||
- 单一来源:**根 `package.json` 的 `version`**,构建时经 esbuild `--define` 注入 agent。
|
||
- 运行时可通过 `system.info` 的 `build` 字段读到:`version` / `commit`(git sha)/ `built_at` /
|
||
`hash`(运行中脚本的 sha256 前 12 位 —— 部署校验用)。
|
||
- 控制端在握手(`auth_ok`)即拿到被控端版本,`nodeagent info` 会显示:
|
||
```
|
||
被控端版本 : v1.0.0 @ ef9ad97 构建 740d51953fd0
|
||
能力对齐 : 42 项,两端一致 ✓
|
||
```
|
||
|
||
## 2. 语义化版本的含义
|
||
|
||
| 变更类型 | 版本位 | 例子 |
|
||
|---|---|---|
|
||
| **破坏性**:删除能力、给既有能力**新增必填参数**、改变既有能力的返回结构、修改认证/握手流程 | MAJOR | 删掉 `screen.find`;把 `fs.read` 的 `path` 改成必填 `remote_path` |
|
||
| **新增**:新能力、既有能力的**可选**参数、新增返回字段、新增审计事件类型 | MINOR | 加 `system.audio.*`、给 `deploy` 加 `--check` |
|
||
| **修复/内部**:bug 修复、性能、文档、不影响协议的重构 | PATCH | 修备份编码、拆分文件 |
|
||
|
||
## 3. 兼容性契约(硬规则)
|
||
|
||
1. **只增不改**:既有能力的**参数名与语义不得变更**;要改就新增能力(如 `screen.find` 与未来的
|
||
`screen.find2` 并存一代),旧能力至少在下一个 MAJOR 才移除。
|
||
2. **不新增必填参数**:既有能力只能加**可选**参数 —— 否则旧控制端一调就 `E_PARAM_INVALID`。
|
||
3. **返回结构只加不减**:新增字段必须可选,旧控制端忽略即可。
|
||
4. **`auth_ok` 的字段只增不减**:v20 新增的 `agent_version`/`build`/`protocol` 均为可选,
|
||
旧控制端不认也不报错。
|
||
5. **协议主版本**(`PROTOCOL_VERSION` 的主号)不一致时被控端直接拒绝连接并返回
|
||
`E_PROTOCOL_MISMATCH` —— 这是唯一的硬闸门,用于承载**无法兼容**的协议变更。
|
||
6. **能力清单是唯一事实源**:控制端不比版本号,而是比**能力集合**
|
||
(`capabilityDiff`)—— 版本号可能相同而内容不同(本地 dev 构建),能力集合不会骗人。
|
||
|
||
## 3.1 v21 新增字段(均为**可选**,向后兼容)
|
||
|
||
| 位置 | 字段 | 含义 |
|
||
|---|---|---|
|
||
| 被控端 `agent.json` | `allow_from: string[]` | 来源网段白名单;**未配置 = 放行全部**(与旧行为一致) |
|
||
| 被控端 `system.info` | `build.cert_sha256` / `network.allow_from` | 证书指纹与白名单状态(供控制端展示与巡检) |
|
||
| 被控端 `auth_ok` | `build.cert_sha256` | 同上(握手期即可得) |
|
||
| 控制端 `nodes.<名>` | `cert_sha256: string` | **钉住**的被控端证书指纹;不一致即 `E_CERT_MISMATCH` 拒连 |
|
||
| 发现报文 | `cert_sha256` | 未认证广播,**仅作参考**,不可当身份凭据 |
|
||
|
||
新增错误码:`E_CERT_MISMATCH (-32422)`、`E_NET_DENY` 类事件走审计 `net.deny`。
|
||
|
||
## 4. 升级顺序建议
|
||
|
||
**先升级被控端,再升级控制端**:
|
||
|
||
| 组合 | 结果 |
|
||
|---|---|
|
||
| 新被控端 + 旧控制端 | ✅ 安全。被控端只做加法,旧控制端不认识的新能力会被忽略 |
|
||
| 旧被控端 + 新控制端 | ⚠️ 可用,但新功能不可用。`nodeagent info` 会列出**远端缺少**的能力并提示升级 |
|
||
|
||
> 若顺序反了也不会坏:控制端调用缺失能力时返回 `E_CAPABILITY_NOT_FOUND`,
|
||
> 而 `nodeagent info` / `connect` 会提前把它变成一句人话提示。
|
||
|
||
## 5. 更新方式(两条路,按可达性选)
|
||
|
||
| 方式 | 命令 | 前提 |
|
||
|---|---|---|
|
||
| 推送式 | `pnpm update:win <设备名>` | 控制端↔被控端**可达**;自动打包含校验与指纹复核 |
|
||
| **拉取式** | `nodeagent update --url <地址> --sha256 <哈希>` | **只要被控端能上网**(含 GitHub Release) |
|
||
|
||
先预检再更新(不动任何文件):
|
||
|
||
```bash
|
||
nodeagent --node win update --url <地址> --sha256 <哈希> --dry-run
|
||
```
|
||
|
||
`--sha256` 是**安全底线**:不校验哈希等于开放远程代码执行。
|
||
|
||
## 6. 发版流程
|
||
|
||
```bash
|
||
pnpm release patch|minor|major|<x.y.z> # 改版本 → 构建 → 打包 → 校验和/清单 → 提交打标签 → GitHub Release
|
||
pnpm release 1.0.0 --dry-run # 演练(无任何副作用)
|
||
```
|
||
|
||
产物(GitHub Release 资产):
|
||
|
||
| 资产 | 用途 |
|
||
|---|---|
|
||
| `agent.mjs` | **自更新载荷** —— 被控端 `system.agent.update` 直接用它(配 `--sha256`) |
|
||
| `nodeagent-win-x64.zip` | 人工安装包(自包含 `node.exe`,免装 Node.js) |
|
||
| `latest.json` | 版本清单:version/commit/built_at/capabilities + 两个资产的 url 与 sha256 |
|
||
| `SHA256SUMS` | 校验和 |
|
||
|
||
> ⚠️ **首次仍需一次性安装**:自更新能力本身得先在机器上(鸡生蛋无解)。
|
||
> ⚠️ **`node.exe`(Node 运行时)不在自更新范围**:只换 `agent.mjs`;需升级 Node 时重跑 `install.ps1`。
|
||
> ⚠️ 本仓库的 `.github/` 改动需要 `workflow` 权限才能推送(`gh auth refresh -h github.com -s workflow`),
|
||
> 发版脚本刻意跳过 `.github/`,避免发版流程被它卡住。
|
||
|
||
## 7. 出问题怎么排查
|
||
|
||
```bash
|
||
nodeagent --node win info # 版本 + 能力对齐情况(先看这个)
|
||
nodeagent --node win invoke system.info --args '{"fields":["build"]}' # 构建指纹
|
||
nodeagent --node win deploy <包> --check # 本地包 vs 远端是否一致(不改动)
|
||
```
|
||
|
||
- **远端过旧**:`info` 会列出缺失能力 → `nodeagent update ...`
|
||
- **本控制端过旧**:`info` 会列出"不认识的新能力" → 更新控制端(`git pull` → `pack:mac` → `install-macos.sh`)
|
||
- **同版本但指纹不同**:属正常(内容相同才叫同版本);用 `--check` 对比实际内容
|
||
- **部署后没生效**:先查杀软是否处置了被替换的 `agent.mjs`(见 `REMOTE-LIMITS.md` 的「杀软误拦」)
|