Files
dsh_shenxian/dsh-server-docs/archive/_tmp_r6_s8.md
T
admin 04776af4b1 docs: 工作区根项目文档批量入仓(04-调整方案 113–128 / 交接单 T09–T21 / ops / archive)
起因:用户 2026-09-17 明确「所有文档都要同步,都放在开发仓库 docs 对应文件夹下」。
判据:工作区根 *.md 中在仓库(git ls-files --quotepath=false)搜不到的那些。

入仓 33 份(一律复制,工作区根原件保留不动,避免引用断链):
- 04-调整方案/113–128(16 份 · 原子占号后落盘):覆盖网络 传输方案取舍 / 应用场景与待完善清单 /
  插件化vs改内核 / 问题逐条推演 / 参数表与观测口径;会合中继拆分取证与改造方案;
  集群化改造方案 Manager-Worker;跨节点迁移与节点自举;项目代码分层范式与迭代风险评估;
  搬运与共享重建方案 guest w47→w106;方案规划方法提炼;文档无效信息审计报告;
  会话接续机制复盘与修复;会话接续规范;dsh 客户端化部署方案;dsh 桌面客户端开发方案
- 交接单/archive/交接单-已完成/T09–T21(13 份 · 覆盖网络线已完成单归档)
- ops/(2 份运行态指针:接续入口 / 接续包 · 覆盖网络线)
- archive/(2 份临时与内部简报)

已排除(无需重复入仓):覆盖网络线 10 份方案正文已入档案 103–112(文件名不同)。

登记:INDEX.md §二 新增 04-113–128 共 16 行 + §四 追加 T09–T21 说明 + 机器摘要行刷新
(⛔ 未跑 docs-index-stats.py --write:该脚本会按 \r\n 归一化全文件行尾,故改为字节级单行替换);
README.md 追加 1 条入仓指针;docs-manifest.json 复跑 scripts/docs-manifest.py 刷新。

验收:工作区根 47 份 .md —— 同名已入仓 8 / 本次内容一致 29 / 已知改名映射 10 / 未入仓 0;
git status 待提交清单只含本次新增与登记 3 件(未涉 src/ 与 relay 代码面)。
2026-09-17 18:24:19 +08:00

168 lines
15 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.
---
## §8 执行回报(执行棒 · 2026-09-17 08:35)—— **已回填**
```
【序 ③ 一机一钥 + 信任根 · 执行回报(2026-09-17 08:35)】
一句话判定: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 任何一行。