Files
dsh_ai1net_server/归档/交接单-20260924-归档/交接单_一机一钥与信任根_20260917.md
T
admin 7bd1151c67 chore(工作区): 归档交接单 27 件 + 新增 CODEBUDDY §9 工作区卫生
交接单按归属约定(正文落文档库、工作区只放指针)归档至
归档/交接单-20260924-归档/,含逐件判定 README:
- 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26)
- 4 件主题已被覆盖网络线入口汇总
- 7 件历史接续包/规划件

CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、
工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
2026-09-24 07:58:57 +08:00

427 lines
37 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.
# 交接单 · 一机一钥 + 信任根(序 ③ / 清单 P0-4)
> **类型**:执行棒(执行会话按本单开工,**不读规划会话的上下文**)
> **产出**:覆盖网络线序 ③ —— 把「共享密钥 + 无成员资格校验」换成「一机一钥 + 离线信任根」的身份基线
> **上游**:`接续入口_覆盖网络线_20260916.md §0/§2`(唯一入口)|**定序**:③ → ④(443 兜底)→ ⑤(参数表·观测·权限评估)
> **前置已完工**:① 会合/中继拆分(S0–S4 + R5)|② 网抽象 + 地址规划 + 引导(P0-1/2/3,单子 `交接单_网抽象与地址规划R6_20260916.md`)⇒ ⛔ **不要重做 ①②**
> **本单不含**:443/TCP 兜底(序 ④)、参数表与观测(序 ⑤)、房间层/presence(第 7 步)
---
## §1 目标
**一句话**:让「每台机器有自己的密钥、由离线信任根授权入网、可单台吊销且不牵动全网」这件事在 47(Manager)与 106(Worker)上真实跑通,并让 relay 的准入从「查共享 secret」变成「按 hostId 查成员资格」。
四条可验收的子目标:
1. **四层密钥模型落地**(形状照抄 Tailnet Lock):根(离线)→ 签名者(在线多把,受根授权)→ 节点密钥(每机一把)→ 会话密钥(内存,定期 rekey)。
2. **入网 = 签名**:节点公钥必须携带一个**受信任签名者**的签名,**各节点在本地校验**,验不过就不建立会话(⇒ 被攻破的控制面**无法插入攻击者节点**)。
3. **relay 准入改造**:`keys` 表/文件从「单一共享凭据」改为**按 `hostId` 索引**,并补上 ② 单带回的待办② —— **「host 属哪张网」的成员资格校验**(缺这一条 = 任何持有任意密钥的 host 都能进同一扁平命名空间)。
4. **吊销与恢复可演练**:撤销单台节点 ≠ 全网换密钥;根密钥有 **≥2 份离线副本**且**恢复演练跑过一次**。
---
## §2 只读前置(⛔ 只读,不改)
**开工三件事(顺序不可颠倒)**:
1. `"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "E:/ProgramData/AI技能/aliyun-dsh-server/state.py"` —— 看锁 / git 基线 / 在途单。
2. 抢锁:`bash "D:/github/dsh_shenxian/dsh-server-docs/scripts/handoff-guard.sh" --claim-exec "<你的会话名>"`。**抢不到 = 有会话在跑 = 停手**(⛔ 不是"等一会儿再抢")。做完立即 `--release-exec`。
3. 读本单 §3 范围与 §4 决策点,确认没有需要现问用户的事项(若真命中红线,按 §4 的升级口径一句话问,⛔ 不许捆包)。
**必读(按此顺序,读多少写多少,别全库摸底)**:
| # | 来源 | 读什么 |
|---|---|---|
| 1 | `覆盖网络_问题逐条推演与解决方案_20260916.md` **§A4(约 73–95 行)** | 本单的**方案正文**:四层密钥模型表 + 恢复路径 + 结论(照抄 Tailnet Lock) |
| 2 | `覆盖网络_应用场景与待完善清单_20260916.md` **P0-4(第 80 行)+ §五 主线(93–103 行)** | 缺口定义(根保管/轮换、私钥丢失恢复、被盗吊销)+ 序 ③ 的前后依赖 |
| 3 | `交接单_网抽象与地址规划R6_20260916.md` 的 **「执行进度」段** | ② 收官口径 + **待办② 原文**(relay `keys` 按 hostId 索引、缺成员资格校验)⇒ 本单 §5-S3 的输入 |
| 4 | `交接单_relay落地R2-R4_20260916.md` **§9–§12** | R2–R5 证据 + **§12 的两条教训**(A1 假 404:**路由表未命中必须补齐再失败关闭,⛔ 绝不静默回退默认机**;A2 `state.py` 锁信号) |
| 5 | 代码仓 `src/net/relay/*`(`server / client / dialer / wire / duplex / main / index`)、`src/config.ts`、`src/db/*`(迁移版本号,当前到 **v8**) | 现状密钥校验点、白名单机制(`DSHS_RELAY_DIALERS`)、迁移写法 |
| 6 | 47 上 `/opt/dshs/relay-keys.json` 与两个 drop-in(均有 `.bak-r5*` 备份) | **现状密钥长什么样**(⛔ 只读;改前必须先备份成 `.bak-r6*`) |
**环境要点(别现查)**:
- 47 = Manager(控制面库 = 47 的 PG13 `127.0.0.1:15432`,单元 `dshs-pg`;**PG 只能 TCP**);106 = Worker `w-106`(`19000`);47 另跑本地 Worker `w-47`(`19100`)。
- **布局不对称**:47 = `/opt/dshs/lib/`,106 = `/opt/dshs-cluster/lib/`;**部署 = 本机 build 后 scp**(⛔ 不在服务器上改码)。
- `npm test` **必须 Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe`)。
- 47 的 bubblewrap = **0.4.0**(`--perms` 属 0.5+ ⇒ 用 `--tmpfs`);106 = 0.11.0。
- 本机 shell:Python stdin/stdout 走 `buffer` 显式 UTF-8;写文件 `newline=""`;含 Windows 路径的 Python 落 `.py` 再跑。
---
## §3 范围
**做**:
- 密钥层次与落点(根 / 签名者 / 节点 / 会话四层)。
- 节点入网凭据的**签发**(签名者)与**本地校验**(各节点)。
- relay 准入改造:`keys` 按 `hostId` 索引 + 成员资格(host ∈ 哪张网)校验。
- **失败关闭**:校验不过 ⇒ 明确拒(`403` / 专用错误码),⛔ **不得静默回退到共享凭据或默认 host**(A1 教训)。
- 吊销路径(撤单台)与轮换路径(节点重签),各跑一次实测。
- 单测:新增用例**先红后绿**(照 A1 的做法),并挂进 `npm test`。
**不做(⛔ 命中即越界)**:
- ⛔ 不自动升级 dsh(R1);⛔ 不改官方 dsh 主程序与缓存(R2)。
- ⛔ 不做序 ④(443/TCP 兜底)、序 ⑤(参数表/观测/权限评估)—— 各自的"信号出现"条件见 §3 末尾。
- ⛔ 不做房间层 / presence(第 7 步)。
- ⛔ 不新开任何公网端口:**47 公网暴露面净增必须 = 0**。
- ⛔ 不重做 ② 的任何一步(P0-1/2/3);⛔ 不重做 R0–R5;⛔ 不碰 `32022`/`32023`。
- ⛔ 不引入第三方 relay / 第三方 CA / 依赖型身份服务(自研路线已定)。
- ⛔ 不 commit、不 push(未获授权)。
- ⛔ **不做「网络变化 ⇒ 重新跑选点、换到更优中继」** —— 判据与代码**已存在**(`src/net/relay/placement.ts`:速度+负载打分 / 满载=唯一硬门 / 失败降权 / 手动不被静默改选,**T15 已验收**),但**未接线**且**当前只有 1 台中继**(方案 §13.5「未做:Manager/门户侧接 `chooseNode()`;relay 集群的多中继选主」)⇒ 归属 **序 ⑥(3–5 台最小形态)之后**。本阶段「网络变化」的既有处置 = **取消剩余退避、立即重拨原地址**(`networkChanges`,**T12 已验收**)。
- ⛔ **不做「应用级(presence / 房间 / 实例会话)断开重连后的状态恢复」** —— 归 **第 7 步**(仓库现**无应用层代码**)。本单只需保证**身份层不成为它的阻塞项**:节点密钥是"换点 / 重连"时唯一需要重新证明的东西。
> ✅ **不属于本单、但也不是缺口**(回答「网络变化 / 断开重连怎么办」类提问时**直接用这段**,⛔ 不要再现查):
> **网络变化 ⇒ 立即重拨原地址**、**断开重连 ⇒ 连接 / 注册 / 路由三级恢复**(**流级恢复有意不做**,交上层幂等重试)—— **均已实现并单测 + 47 真机验收**,证据 = 方案 **§12.2(五类场景×处置×恢复时间)/ §12.3(四级恢复语义)/ §12.4(T8–T15 + 真机输出)**。
> ⇒ ⛔ **不要在本单里重做**;⛔ **也不要"顺手"给流级加重放日志** —— §12.3 ④ 明确判「不做」:*假装能做 = 制造"看起来恢复了其实数据烂了"*。
> ⚠️ 唯一**未覆盖**的一层是**应用级状态**(见上一条),而那是第 7 步的事,不是本单的债。
**越界信号(出现即停手,写进 §8)**:
- 需要**扩大权限面**(新挂载 / 放开遮蔽 / 暴露平台目录或 env / 放宽 nft / 提档位)⇒ **R5**,先出「权限影响评估」再问一句话。
- 需要**外部玩家入网**(P0-5 缺口 B)⇒ 那是独立一条,⛔ 不在本单内顺手做。
- 需要 **VPN/代理类节点降级**、**443 兜底** ⇒ 记入 §8 的"交给序 ④",不在此实现。
---
## §4 决策点
### 4.1 已定项(规划棒已拍,执行棒**不得自行更改**;要改必须回写 §8 并说明理由)
| # | 决定 | 理由 |
|---|---|---|
| **D1** | **凭证形状 = 节点公钥 + 签名者签名信封**(自签、无 x509、无第三方 CA) | 照抄 Tailnet Lock 的形状(§A4);x509 会引入 CA 生态与吊销列表搬运成本,而我们只需要"授权入网"一个语义 |
| **D2** | **校验位置 = 节点本地**(relay 侧只做**辅助**准入,不替代本地校验) | §A4 的核心:控制面被攻破仍插不进节点。若把校验只放在 relay,等于把根信任交给控制面 |
| **D3** | **根密钥离线**,在线签名者拿**根授权**;签名者可多把(≥1 把在 47) | §A4 四层模型;根在线 = 单点被攻破即全网伪造 |
| **D4** | **relay `keys` 改造与成员资格校验并入本单** | 入口 §0 已定序理由:② 待办② 与 ③ 天然衔接,拆成两棒会让"hostId 索引"和"签名校验"各改一遍同一处代码 |
| **D5** | **失败关闭**:任一校验未命中 ⇒ 显式拒绝 + 专用错误码,**零请求发往默认机** | A1 教训(R2-R4 单 §12):静默回退 = 假 404 之源 |
| **D6** | 迁移走**新版本号 v9**(当前 v8),⛔ 不改历史迁移 | 项目既有迁移纪律 |
### 4.2 交给执行棒自决(⛔ **不上升为提问**)
> 判据:这些都有客观可判的优劣 ⇒ 自己排完序、选第一名、做掉,并在 §8 里写一句「我选了什么(可推翻)」。
- **密钥文件介质**:`0600` 文件 / 系统密钥库 / 环境变量 —— 按"可备份性 × 权限最小 × 与现有 drop-in 机制一致"排序自选。
- **节点密钥算法**:Ed25519(默认)vs 其他 —— 无特殊理由就用 Ed25519。
- **relay 侧 `keys` 的存储形态**:`relay-keys.json` 就地升级 vs 新增一张表 —— 以"能原子替换 + 可回滚"为先。
- **新增错误码 / 表结构 / 文件命名的具体取值**。
- **重试与超时参数**(在 §6 验收约束内自选)。
### 4.3 真需要用户拍板的(**命中才问,且一轮只问这一句**)
1. **根密钥的离线保管人/介质**(纸质恢复码放哪、离线副本存哪台设备)—— 这涉及**用户手头的物理资产**,不是技术选型可替代。
- 若用户此前未指定:**执行棒按"本机 + 其一离线副本"落默认并继续**,在 §8 里写清"现落点 = X,可推翻"。
- **根密钥的用途边界(⛔ 别搞错)**:它**只用于授权 / 撤销「签名者」** —— **不签发节点、不加密数据、不参与会话**。⇒ "根在线"**不带来任何性能问题**,唯一影响是 **安全**(泄露 ⇒ 可自行授权签名者 ⇒ 可插入任意节点,节点本地校验也拦不住,因为签名合法)与 **恢复**(丢失 ⇒ 极端情况下"全网重置")。
- **落点候选与判据(开发期用 C,收敛目标是 D;⛔ E 直接排除)**:
· **A 纸质恢复码**(打印/手抄种子,实体保管)—— 优:真离线、零电子攻击面、不受系统重装 / 云账号被锁 / 设备报废影响,成本≈0。劣:怕水火与搬家丢失、平台无法代管、**被拍照即永久泄露**(不像密钥文件还能改权限)。
· **B 离线设备上的加密 keystore**(U 盘 / 旧手机 / 离线笔记本 + 口令)—— 优:可机器读取、便于早期频繁签发、可存多份并校验完整性。劣:U 盘会坏 / 接口会淘汰、**一旦插上联网机就不再"离线"**、弱口令=等同于 A 泄露但更难发现。
· **C 本工作区开发机的 `0600` 文件**(如 `~/.dshs/root.key`)—— 优:零额外成本、可直接脚本化、恢复演练最省事,开发期推进最快。劣:**本机不是沙箱且日常联网**(浏览器 / 编辑器 / agent 同机)⇒ 泄露面大;⚠️ 它与"签名者"的天然落点**同机** ⇒ 四层塌成两层,**Tailnet Lock 的核心收益(控制面/签名者被攻破仍插不进节点)直接归零**。⇒ 仅作**开发期**落点,必须标注"非最终"。
· **D 两份异介质、异地副本**(本机加密文件 + 纸质码 / 离线 U 盘)—— 优:满足 §A4「**≥2 份离线副本**」硬要求、单份损毁不致命、恢复演练可只动一份。劣:需维护两份一致(轮换时漏更新一份=埋雷)、保管成本翻倍;⚠️ 两份放在**同屋 / 同云账号**等于只有一份。
· **E 平台服务器(47)** —— **不建议**:直接违反"根离线"这条设计本身;47 公网可达且已开 443 入站 ⇒ 攻击面最大;被攻破即**同时**拿到根 + 签名者 ⇒ 攻击者可**另造一张网**;且在 R5 上属"权限面 / 暴露面"变更,要单独出评估。
2. **是否允许为校验链路新增一个常驻进程/单元** —— 只有在其**扩大公网暴露面或占用新端口**时才需问(否则命中 D1/D2 已定项,自决)。
### 4.4 技术实现裁决顺序(照 `dsh-decision-method §4.4`)
选型/写法冲突时依次看:**① 权限最小**(R5 精神)→ **② 失败关闭**(D5)→ **③ 可回滚** → **④ 与现有一致**(不新造第二套机制)→ **⑤ 性能**。
---
## §5 步骤(S0–S5,每步都要有可复跑的验收)
> 纪律:**先单点验证再推广**;每步做完**立即写证据**(命令原文级),别攒到最后;**改任何文件前先确认已在 §2 第 2 步抢到的锁内**。
> 备份纪律:47 上改动前,先把被改文件备份为 `*.bak-r6-<实际时间>`;回滚见 §7。
### S0 · 只读取证(⛔ 不改任何东西)
- 摸清**现状密钥校验点**:`src/net/relay/*` 里"谁能连上 relay"是怎么判的、`DSHS_RELAY_DIALERS` 白名单在哪判、`relay-keys.json` 的实际结构。
- 摸清**现状配置面**:`src/config.ts` 里与 relay 身份相关的项;47/106 两个 drop-in 的 env 差异。
- 摸清**DB 迁移写法**(`src/db/*`,看 v8 是怎么写的)。
- **出口判据**:能用一段话画出"现在的身份链路"(谁拿什么凭据、在哪判、判不过会怎样),且**指出至少一处"判不过会静默通过/静默回退"**(若无,说明取证不足)。
### S1 · 四层密钥模型落地(本机)
- 定义并实现四层的**载体**(根 / 签名者 / 节点 / 会话)—— 会话层若已有隧道 rekey 机制则复用,⛔ 不新造。
- 节点密钥**每机一把**,落点与权限(0600、属主正确)。
- **出口判据**:`npm test` 绿;能列出两台机器各自的节点公钥指纹。
### S2 · 签发与本地校验
- 签名者签发节点入网凭据;节点**本地校验**通过才建会话。
- **先红后绿**:先写"签名不对 ⇒ 拒绝"的用例(**必须观察到红**),再实现。用例入 `test/`。
- **出口判据**:① 单测含"篡改签名 ⇒ 拒绝"且**先红后绿**有记录;② 手工构造一个"未授权节点"⇒ **建不起会话**,且**日志里有明确拒绝原因**。
### S3 · relay 准入改造(`hostId` 索引 + 成员资格)
- `keys` 从共享凭据改为**按 `hostId` 索引**;补 **host 属哪张网** 的成员资格校验(输入 = ② 单待办②)。
- **失败关闭**:未命中 ⇒ 拒 + 专用错误码,**零请求发往默认机**。
- **出口判据**:① 合法 host 正常连通(47 本地 + 106 跨机各一例);② 「拿 A 网的凭据进 B 网」被拒;③ 「伪造/未登记 hostId」被拒;④ 三种拒绝路径**均有可观测日志**。
### S4 · 吊销与恢复演练
- **吊销单台**:撤销 w-106 的节点签名 ⇒ w-106 被拒;47 与 w-47 **不受影响**(这就是"撤销单台 ≠ 全网换密钥")。
- **轮换/重签**:w-106 重新签发后**恢复连通**。
- **根密钥恢复演练**:用**离线副本**重建信任根,走一次完整流程并记录耗时与踩坑。
- **出口判据**:三条演练**全部有命令原文级输出**;恢复演练结论可为"通过"或"未通过(写清卡在哪)",⛔ 不得只写"应该可以"。
### S5 · 端到端复验 + 不退化检查
- 复验**用户可见面**:`admin`(w-47) 与 `guest`(w-106) 两个实例页 **200**;跨机文件面落盘仍 OK。
- **不退化判据**:47 公网暴露面**净增 = 0**;`w-106` 仍 `via='relay'`;relay 常驻未掉。
- 对账:本机 `lib/` vs 47 `/opt/dshs/lib/` **0 差异**(照 R5 的 29/29 对账做法)。
- **出口判据**:§6 全部判据逐条打勾,未过的条目**必须写明卡在哪 + 什么条件一出现必须回头解决**(⛔ 不许用"暂时接受"糊过去)。
---
## §6 验收(判据清单)
| # | 判据 | 怎么测 | 过 / 不过 |
|---|---|---|---|
| 1 | 每台机器有独立节点密钥,指纹互不相同 | 打印两机公钥指纹比对 | ☐ |
| 2 | 篡改签名的入网凭据**建不起会话** | 单测(**先红后绿**)+ 手工构造 | ☐ |
| 3 | **未授权节点**被拒,且**日志有明确原因** | 手工构造未授权节点连接 | ☐ |
| 4 | relay 准入**按 hostId**,且校验**成员资格** | 三例:合法 / 跨网 / 未登记 | ☐ |
| 5 | **失败关闭**:任何未命中 ⇒ 显式拒绝,**零请求发默认机** | 判别器 = relay 有无 `DIAL`;辅以 `not_found=0` | ☐ |
| 6 | **撤销单台不牵动全网**:撤 w-106 ⇒ 106 拒、47 与 w-47 正常 | S4 吊销演练输出 | ☐ |
| 7 | 重签后**恢复连通** | S4 轮换演练输出 | ☐ |
| 8 | 根密钥 **≥2 份离线副本** + **恢复演练已跑** | 副本清点 + 演练记录 | ☐ |
| 9 | 密钥文件权限 `0600`、属主正确 | `stat` 输出 | ☐ |
| 10 | **不退化**:两实例页 200、跨机文件面 OK、`w-106 via='relay'` | 端到端复验 | ☐ |
| 11 | **47 公网暴露面净增 = 0** | 端口清点前后对比 | ☐ |
| 12 | `npm test` 全绿(含新增用例) | Node 22 跑 | ☐ |
| 13 | 本机 `lib/` vs 47 `/opt/dshs/lib/` **0 差异** | 逐文件 hash 对账 | ☐ |
**判别器(复用,⛔ 别自创)**:静默失效一律用**判别器**定位 —— ① relay 有没有 `DIAL`(没有 = 请求根本没出去);② 临时监听目标口把请求接住;③ 端点翻译只认 `dsh_hosts.via` 原文(⛔ 不是 `reachability.via`)。
---
## §7 回滚
**粒度:能单步回滚的单步回滚**,⛔ 不整包推倒。
| 层级 | 动作 | 判据 |
|---|---|---|
| **配置层** | 恢复 `*.bak-r6-*` → `systemctl daemon-reload` → `systemctl restart dshs` | 两实例页回落 200 |
| **relay 侧** | 恢复 `relay-keys.json` 旧表(改前必备份)→ 重启 relay 单元 | 47 本地 + 106 跨机连通恢复 |
| **DB 迁移层** | v9 回滚 = 恢复表结构 + 重启(迁移脚本必须自带"回滚段"或至少一份导出) | `dsh_hosts.via` 等字段仍可读 |
| **代码层** | 回到改前 commit(工作区当前有未提交改动 ⇒ **先 `git stash` 或导出一份 diff 存档**,⛔ 不许 `checkout --` 抹掉别人的改动) | `npm test` 绿 + 端到端复验 |
| **全量兜底** | cluster 回滚 = 删 drop-in `dshs.service.d/cluster.conf` → `daemon-reload` → `restart dshs`(⚠️ cluster 配置在**这个 drop-in 里**,⛔ 不在 `/etc/dshs.env`) | 退回单机 Manager 形态 |
**⛔ 回滚禁区**:不许用 `rm -rf` / 通配符清目录;不许 `git add -A`;不许对 `INDEX.md` 跑 `git checkout --`。
---
## §8 回报格式(执行棒按此格式收口)
```
【序 ③ 一机一钥 + 信任根 · 执行回报(YYYY-MM-DD HH:MM)】
一句话判定:<做到哪一步 / 全绿 / 卡在哪>
改了什么(文件级,含行号):
· <本机:路径:行 —— 一句话>
· <47 / 106:drop-in / 文件 —— 一句话>
· 备份:<*.bak-r6-* 落点>
验收证据(命令原文级,逐条对 §6 编号):
· [1] <命令 ⇒ 关键输出>
· [2] ...
· [未过] [N] 卡在哪 = <证据>;已做到 = <到哪>;什么条件一出现必须回头解决 = <条件>
我选了什么(可推翻):
· <§4.2 的每一项一行>
未做 / 越界记录:
· <交序 ④ 的:443 兜底 | 交序 ⑤ 的:参数表/观测/权限评估 | 命中 R5 待评估的:…>
下一棒建议:<一句话 + 该读哪份文档>
指纹:md5(<本单路径>) = <hash>;54/106 产物 hash 对账 = <n/n>
```
**收口动作(缺一即视为没做完)**:
1. 释放锁:`--release-exec`(⛔ 带锁结束 = 把所有人挡在门外)。
2. **登记下一棒的一次性 automation**(`automation_update`,mode=create,scheduleType=once,约 +2 分钟),prompt 只写「跑 `state.py`,按 **覆盖网络线** §2「本轮动作」块开工 + 本轮只做一件事做完即停」——⛔ **不许把任务细节抄进 prompt**。
3. 在给用户的回复里**用陈述句**告知:已登记自动接续、约 N 分钟后自动开新会话、接续点 = X。
4. 产出物调 `present_files` 交付。
5. ⛔ 不 commit、不 push。
---
## 附:本单相关的硬约束(复述,防走偏)
- **R5** 权限只准收窄;**R7** 只做被明确要求的事,额外发现的问题**先报告后动手**;**R10** 实例起不来先查属主/EACCES 再怀疑 OOM。
- **hook 脚本**必须走 `sys.stdout.buffer.write(bytes)`(文本层 `write` + 非 UTF-8 文案 ⇒ 静默放行)。
- **两条命令坑**:`pkill -f` 匹配实际 argv;`fetch` 静默丢 `Host` 头(多租户验证必须 `curl -H "Host: …"`)。
- **两条取数坑**:本机 `md5sum` 输出 `hash *path`、远端 `hash path`(先归一化再 diff);`ssh '…'` 单引号里不能出现单引号(用 `cat > f <<"EOF"` 落文件)。
- **本机 `refs/remotes/**` 写不进** ⇒ 分叉判定必须 `git ls-remote origin refs/heads/master` 取裸 sha(本单不涉及 push,仅备查)。
---
## §8 执行回报(执行棒 · 2026-09-17 08:32)—— **已回填**
```
【序 ③ 一机一钥 + 信任根 · 执行回报(2026-09-17 08:32)】
一句话判定:S0–S5 全走完。判据 [1]–[9] / [11] / [12] / [13] **全过**;判据 [10] **部分未过**
—— guest(w-106) 实例页 502,真因在**平台的远端实例凭据落盘**(`landModels →
writeHomeFile` 用**本地路径**写 w-106 的 home),**与序 ③ 无关**(覆盖网络侧已用
「经 relay 落点直连 w-106:21000」证明通路完好,见 [10])。
改了什么(文件级,含行号):
· 本机(代码仓 D:\github\dsh_shenxian)——
- `src/net/relay/identity.ts`(**新建** 657 行):四层密钥模型(根→签名者→节点→会话)+
签发/验签/吊销/本地校验的**纯函数与 IO 分区**;判据全在本模块(relay 只做辅助准入)。
- `src/net/relay/keys.ts`(**重写**):`RelayKeyEntry{network,secret}` + `RelayKeyMap`;
键从 `hostId` 改**逻辑名 `<net>/<hostId>`**;新增 `normalizeKeyRecord` / `isQualifiedName`
/ `lookupKey` / `describeKeyEntry` / `keyEntryOf`;`parseKeysInline` 切点 = **最后一个 `:`**。
- `src/net/relay/server.ts`(4 处):握手改 `lookupKey(keys, network, hostId)` +
`entry.network !== network ⇒ deny('network-mismatch')`;MAC 之后新增身份校验块
(`identity-no-trusted-signers` / `identity-incomplete` / `identity-${reason}`);
`authed()` 计数新增 `identityOk` / `identityRequired` / `trustedSigners` / `revokedHosts`。
- `src/net/relay/client.ts`:新增 `identity?` 字段与 `identityFields(challenge)`
(**MAC 输入串一字未改**,只追加字段)。
- `src/net/relay/main.ts`:`loadIdentityForServer()`(强制 + 无签名者 ⇒ **起动即抛**)
+ client 侧装配 `loadClientIdentity`。
- `src/net/relay/index.ts`(barrel 同步);`src/config.ts:513-531` 新增
`overlayNetworkId` / `overlayDirTrustedKeys` / `overlayNodeKeyFile` / `overlayNodeGrantFile`。
- `src/worker/relay-tunnel.ts`(+`identity` 选项)/ `src/worker/agent.ts:188`(接线)/
`src/web/server.ts`(Manager 拨号通道接线)。
- `scripts/overlay-keyring.cjs`(**新建** CLI):init-root / init-signer / init-node /
sign-signerset / issue-grant / sign-revocations / verify-grant / recover-root;
本次补 `sign-signerset --issued-at` 与 `recover-root` 的**判据②③**(原先 docstring 写了
三条判据、实现只有①,属文档与实现不一致,已补齐)。私钥一律 `writeSecret()` → 0600,
stdout 只打印公钥/指纹。
- `test/overlay-identity.test.mjs`(**新建** 25 例:A1–A4 签名者集合 / B1–B12 凭据校验 /
C1–C3 keys 带网 / D1–D7 端到端含 D5 **先红后绿** / E1 文件面);
既有测试按新语义更新:`test/relay.test.mjs`(T2)、`test/overlay-auth.test.mjs`(A6)、
`test/overlay-network.test.mjs`(U3/U4/U5);`package.json` test 脚本挂上 identity 用例。
· 47(Manager + relay)——
- 代码:`/opt/dsh-relay/lib/**`(备份 `lib.bak-r6-20260917-0815`)、
`/opt/dshs/lib/**`(备份 `lib.bak-r6-20260917-0820`);relay 对账 26/26、dshs 对账 148/148 全同。
- 密钥:`/etc/dshs/overlay-signer-key.pem`(600)+ `.pub`|`node-manager.key`(600)、
`node-w47.key`(600)|`node-manager.grant.json` / `node-w-47.grant.json` /
`node-w-106.grant.json`|`overlay-signers.json`(根签)|`revocations.json`(签名者签,当前空)。
- 单元:`dshs.service.d/identity.conf`(**新**,Manager 带凭据);
`dshs-relay.service.d/identity.conf`(**改**,备份 `.bak-r6-20260917-0830`;现为
ROOT_PUBKEYS + SIGNER_SET_FILE + REVOCATIONS_FILE + **REQUIRE_IDENTITY=1**)。
- `/etc/dshs-worker.env` 追加 2 行(`DSHS_OVERLAY_NODE_KEY_FILE` / `..._GRANT_FILE`)。
· 106(Worker)——
- 代码:`/opt/dshs-cluster/lib/**`(备份 `lib.bak-r6-20260917-0820`)。
- 密钥:`/etc/dshs/node.key`(600,**指纹 9a0189a4fb4e52f9**)→ 轮换后 `node.rot.key`
(600,**指纹 8cc227320405b986**);`node.grant.json` / `node-w-106.rot.grant.json`;
`overlay-signers.json`。
- `/etc/dshs-worker.env` 改 2 行(备份 `/etc/dshs-worker.env.bak-r6-20260917-0825`)。
· 本机(离线根):`E:\ProgramData\.dshs\{root.key(0600), root.pub, root.recovery-code.txt(0600)}`
· 备份:`lib.bak-r6-20260917-0815`(relay)/ `lib.bak-r6-20260917-0820`(47 dshs + 106 cluster)/
`identity.conf.bak-r6-20260917-0830`(relay drop-in)/ `dshs-worker.env.bak-r6-20260917-0825`(106)
指纹(一次性记全,后续对账用):
· 离线根 pub `bd6d1219571aa70904b227d3e5e86c3d0c08125312d1b5f9e31da4d71b914dd3` / fp `3f6523302720c531`
· 在线签名者 pub `bad464dfd53048efe7b8531029b3030eda49bc12930703d3a2a8f60e3a7daddf` / fp `ca6e5a1e329c22b5`
· 节点 manager fp `b60215af4c12f835`|w-47 fp `83269876c41644c0`|w-106 fp `9a0189a4fb4e52f9` → 轮换 fp `8cc227320405b986`
验收证据(命令原文级,逐条对 §6 编号):
· [1] 每机独立节点密钥、指纹互不相同 ⇒ 三份 grant 的 nodeKey 互不相同:
manager `2e1c…`(fp b60215af4c12f835) / w-47(fp 83269876c41644c0) / w-106(fp 9a0189a4fb4e52f9);
轮换实验再证「一机一钥可换」:106 `init-node --key /etc/dshs/node.rot.key` ⇒
`✓ 节点公钥:5e65572611ad6a37c487136086d7852e6fb29c7f530b3a37161eded57f31bf6a` / fp `8cc227320405b986`。
· [2] 篡改签名的入网凭据建不起会话**(先红后绿)** ⇒ `test/overlay-identity.test.mjs` B 组 12 例
(签名篡改 → `bad-signature`;凭据搬 hostId → `host-mismatch`;换密钥 → `key-mismatch`;
过期 → `expired`)+ D5「门关时同一份坏凭据能注册(红)→ 门开时被拒(绿)」。
· [3] 未授权节点被拒且**日志有明确原因** ⇒ 真机两条独立证据:
relay 侧 `[relay] AUTH DENY remote=127.0.0.1:33802 why=identity-revoked-host retryable=false`(×14);
106 侧 `[relay-client] HELLO rejected reason=identity-revoked-host retryable=false clockSkew≈189ms`。
· [4] relay 准入按 hostId + 校验成员资格 ⇒ `keys.ts` 键改**逻辑名**后:
合法(`AUTH OK host=ops/manager` / `host=ops/w-106`);跨网(`network-mismatch`,且
`overlay-auth` A6 / `overlay-network` U3/U4/U5 四处既有测试按**新语义**更新并通过);
未登记 hostId(`keys` 层即拒)。三种路径均有 `AUTH DENY why=…` 日志。
· [5] 失败关闭、零请求发默认机 ⇒ 吊销态下 guest 请求:
dshs 侧 `[relay-dialer] 拨 ops/w-106:19000 失败:relay client: dial w-106:19000 refused: target-offline`;
relay `refused` 计数 0→4;**未见任何回退默认机**(A1 的 `ensureHost` 失败关闭仍在位)。
· [6] 撤销单台不牵动全网 ⇒ 吊销 w-106 后:
`counters = {"authed":1,"authFailed":14,"refused":4,"identityOk":1,"identityRequired":true,"trustedSigners":1,"revokedHosts":1}`;
`online = ["manager"]`(**manager 会话 upForMs=180681 全程未断**);
`[恢复态] admin 实例页(w-47)HTTP 200 time=0.050s`;`dsh_hosts`:`w-47|local|ops|up`。
· [7] 重签后恢复连通 ⇒ 重签**空**吊销清单 + `restart dshs-relay`:
`[identity] 吊销清单:net=ops hosts=[] nodeKeys=0` → `[relay] AUTH OK host=ops/w-106 session=63607538827ee044 ports=[19000]`;
`counters`:`identityOk=3 / identityRequired=true / revokedHosts=0`;`online=["manager","w-106"]`。
🔑 附一条**更强的性质**:先在 106 上**轮换节点密钥**(换新 grant)后**仍被拒**
(`HELLO rejected reason=identity-revoked-host`)⇒ 证明「**撤 hostId 比换密钥更强**」。
· [8] 根密钥 ≥2 份离线副本 + 恢复演练 ⇒ **演练三判据全绿,耗时 794 ms**:
`✓ [判据①] 与期望根公钥逐字节一致`;
`✓ [判据②] 重建的根签出的 SignerSet(1 把)经原根公钥验签通过`;
`✓ [判据③] 签名与原根私钥签出的逐字节相同`(Ed25519 确定性 ⇒ 排除"看着像其实不是同一把")。
⚠️ **副本数 = 2 份「离线保管载体」但都在本机**:`root.key`(0600) + `root.recovery-code.txt`(0600,96 hex);
真离线介质(U 盘 / 纸)需你亲落 ⇒ 本条判 **部分过**。
(演练从**恢复码**重建,**不使用**原根文件;原根仅用于产出待比对的基准签名。演练产物含一份根私钥副本,已即时删除。)
· [9] 密钥文件权限 0600、属主正确 ⇒ 47 `600 root:root`:`node-manager.key` / `node-w47.key` /
`overlay-signer-key.pem` / `overlay-dir-key.pem` / `relay-keys.json`;106 `600 root:root`:
`node.key` / `node.rot.key` / `relay-keys.json`。(`.pub` 与**已签名清单**为 644,不含秘密。)
· [10] 不退化 ⇒ **三条过、一条未过**:
✅ admin(w-47) 实例页 `HTTP 200 time=0.050s`;
✅ `dsh_hosts`:`w-106|relay|ops|up`(`via='relay'` 保持);
✅ relay 常驻未掉(`systemctl is-active dshs-relay` = active,`identityRequired=true`);
✅ **覆盖网络侧端到端活证**:经 relay 本机落点 `curl -H "Host: guest.alotbuy.com" http://127.0.0.1:34539/`
⇒ `HTTP 401 68B`,与 **106 本机直连 `http://127.0.0.1:21000/` 的 `401 68B` 完全一致**
(relay `endpoints` 同时给出 w-106 的 `19000` 与 `21000`,`streams>0`、`in/out` 有流量);
⛔ **未过** guest(w-106) 实例页 = **nginx 502(0.70 s,确定性复现,非超时)**。
· [11] 47 公网暴露面净增 = 0 ⇒ 公网监听集合 =
`0.0.0.0:22 / 0.0.0.0:80 / 0.0.0.0:443 / 0.0.0.0:58888(BT-Panel) / 0.0.0.0:888 / 0.0.0.0:8765 / [::]:22`
—— **全是原有的**;序 ③ 新增的只有**回环**口:`127.0.0.1:20080`(relay)与 `127.0.0.1:25000+`(拨号落点池)。
· [12] npm test 全绿 ⇒ Node **v22.22.2**:`# tests 130 / # pass 129 / # fail 0 / # cancelled 0 / # skipped 1`
(31914 ms),随后 `verify-inject` 六项全 `✓`、`结论:全部合格 ✅`。
· [13] 本机 lib vs 47 /opt/dshs/lib **0 差异** ⇒ `local=148 remote=148`,路径不对称 0,内容不同 0;
另:relay 侧 `local=26 remote=26`,0 差异;
`scripts/overlay-keyring.cjs` md5 `0727cd436c3b923e48d0c8dd654d2ac4` **三处一致**(本机 / 47 / 106)。
· [未过] [10] 卡在哪 = ① `dsh_instances` 里 guest 记录 `status=stopped`(真机上实例却在跑),
⇒ 平台走 `RemoteSpawner.launch`;② 该路径上 `landModels → home-files.js:45 writeHomeFile`
打开 **`/var/lib/dshs/users/4092b965-…/home/.credentials.yaml`** 时 `ENOENT`
—— 这是**47 本地路径**,而 guest 的 home 自 09-16 22:51 起已在 **106**(47 上该目录只剩 `ws/`);
日志原文:`model landing failed, falling back to env injection Error: ENOENT: no such file or directory, open '…/home/.credentials.yaml'`,
栈:`writeHomeFile(home-files.js:45) → landModels(server.js:172) → resolveApiKey(server.js:200) →
RemoteSpawner.launch(remote-spawner.js:169)`;③ 该请求在 dshs 侧 `incoming request` 之后**无
`request completed`**,由 nginx 返 502。
**为什么判定与序 ③ 无关**:失败点在「凭据写 home 的本地路径」,与 relay 身份准入无交集;
且同一时刻的覆盖网络证据全绿 —— relay 已建立到 w-106 的 `19000`/`21000` 两条隧道、
`dial up` 成功、经隧道取实例回 401(活)。**序 ③ 只动了 identity / keys 逻辑名 / relay 准入 / 配置,
未触碰 `landModels`、`home-files`、`remote-spawner`、`dsh_instances` 状态机。**
已做到 = 网络面与身份面全部证明完好、admin 面 200、平台与实例两侧均无崩溃(`dshs` NRestarts=0、
`dshs-worker` NRestarts=0)。
什么条件一出现必须回头解决 = **① `landModels/writeHomeFile` 改为经 `RemoteUserFs` 写远端实例 home
(或平台对「实例已在跑但 DB 记 stopped」做状态对账);② 或 guest 实例状态回到 `running` 后,
立即重测判据 [10] 的 guest 页**。⇒ 建议并入序 ④ 的相邻缺陷清单,**不在本单范围内顺手改**(R7)。
我选了什么(可推翻):
· 密钥表键 = **逻辑名 `<net>/<hostId>`**(不是裸 hostId)—— 因 U5 暴露"同 hostId 无法存在于两张网"是净变差。
· 受信根 = `DSHS_OVERLAY_ROOT_PUBKEYS` **独立于** `DSHS_OVERLAY_DIR_PUBKEYS`(⛔ 不合并:合并等于让"能换地址的人"顺带能加签名者)。
· 复用 `directory.ts` 的 Ed25519 原语,**不新造第二套签名机制**(符合 D1/D6)。
· 强制身份**本轮就开**(`REQUIRE_IDENTITY=1`)而非留到下一轮:三台节点均已带身份且实测 `identityOk=3`,
吊销演练**只有开强制才可观察**;回滚 = 删该两行 + `daemon-reload` + `restart dshs-relay`(单步)。
· 106 节点密钥**就地轮换**(`node.rot.key` 取代 `node.key`,env 指向新文件)—— 顺带证明"一机一钥可换"。
· 密钥仪式工具**部署到 47/106**(`/opt/dsh-relay/scripts/`、`/opt/dshs-cluster/scripts/`),
因为**签名者私钥不出机器** ⇒ 签发/吊销必须在 47 上跑。
· 报告与单子同文件(§8 就地回填),不另开文件。
未做 / 越界记录:
· 交 **序 ④** 的:443/TCP 兜底(本单未碰)。
· 交 **序 ⑤** 的:参数表 / 观测 / 权限评估(本单未碰)。
· **命中 R5 待评估的:无** —— 本次所有改动都是**收窄**(relay 从"任何持任意密钥者都能声称任意合法网名"改为
"凭据 + 成员资格 + 吊销"三级;公网暴露面净增 0)。
· 额外发现、**只报告未动手**(R7):
① [10] 的平台侧远端凭据落盘缺陷(见上);
② `/opt/dshs/mksess*.cjs` 仍写 **`/var/lib/dshs/dshs.db`(SQLite,仅回滚用)**,
而权威库已是 47 的 PG13 ⇒ **该工具已失效**(本次改用 PG 直插临时 session 完成 R4 合规验收,用完即删,残留 0)。
· 本单**未 commit / 未 push**(按约束 4)。
下一棒建议:**序 ④ · 443/TCP 兜底** —— 先出**规划棒**(给序 ④ 出一份可执行交接单),
读 `D:\github\dsh_shenxian\dsh-server-docs\` 里 `覆盖网络_应用场景与待完善清单 §五` +
工作区根 `接续入口_覆盖网络线_20260916.md §0/§2`;⚠️ 把上面 [10] 的两条额外发现并入其相邻缺陷清单。
指纹:md5(本单) = 260a2e84a8a482cf08705885735986ee(回填前);本机/47/106 产物 hash 对账 = 148/148 + 26/26 + 3/3 全同
```
> 📌 **回填说明**:上面代码块是**本节交付物本身**(§8 要求的报告),未改动 §1–§7 任何一行。