# 交接单 · 覆盖网络主线 ②「网抽象 + 地址规划 + 引导」(**执行会话专用** · 2026-09-16 23:4x) > **口径指纹(必须校验)**:`tail -n +4 交接单_网抽象与地址规划R6_20260916.md | md5sum` = `0d3f1f305e9e6219afa97cdfecff05ba` > 不符 ⇒ **停下,只报告「口径已更新,需重新接续」**。 --- ## 执行进度(⛔ 下一棒先读这一段,别从头重做) | 步 | 状态 | 证据 / 落点 | |---|---|---| | **Step 1 · P0-1 网抽象** | ✅ **已完成并端到端验收**(2026-09-17 00:0x–00:15) | 新增 `src/net/relay/network.ts` + `test/overlay-network.test.mjs`(7 条);会话/端点表按逻辑名索引;DIAL 双门(本网白名单 + 同网);`dsh_hosts.network_id`(SQLite V9 / PG V9 已迁);单测 38/38;产物铺 47 并 md5 逐同 | | **Step 2 · P0-2 引导三级链** | ✅ **已完成并端到端验收**(2026-09-17 06:45–07:05) | 新增 `src/net/relay/directory.ts` + `src/web/routes/overlay.ts` + `test/overlay-bootstrap.test.mjs`(9 条);目录端点 `GET /dshs-overlay/bootstrap`(Ed25519 签名,`no-store`);客户端取址 = env > 缓存 > 种子(**origin 优先取缓存里的 `bootstrap[]`** ⇒ 引导地址可在线轮换);47 已清 `DSHS_RELAY_URL` **仍正常起来**;单测 47/47;六件产物 md5 与 47 逐同 | | **Step 3 · P0-3 名字解析与授权** | ✅ **已完成并端到端验收**(2026-09-17 07:2x–07:4x) | 逻辑名 `/` 成为 `place`(`parseReachability`)/ `resolve`(`Rendezvous.resolve`)的**唯一入参**;控制面**所有内存键**(地址 / via / 端口 / relay 端点 / 拨号口池)改逻辑名;`Reachability` 加 `networkId`;新增 `assertSameNetwork`;`RelayDialer` 加**跨网失败关闭**(带日志);`agentBaseUrlOf` 在 `via=relay` 时**禁回落 endpoint**;relay 跨网拒绝**对外统一 `target-offline`**(不泄露对端清单,区分只留服务端日志)。新增 `test/overlay-auth.test.mjs`(7 条);单测 **64/64** | > ✅ **本单三步已全部执行完毕(2026-09-17)**。下方 §4/§5 保留为**当时的规格**(回看用); > 下一步的依据不再是本单 —— 见 `接续入口_覆盖网络线_20260916.md` 的"下一棒"。 ⚠️ 指纹已随本段更新(改了正文 ⇒ 指纹连带);`接续入口_覆盖网络线_20260916.md` 的引用行已同步。 --- > **上游依据(⛔ 先读它们,别凭本单复述 —— 本单只做"收敛成可执行"这一步)** > - 主线与优先级:`覆盖网络_应用场景与待完善清单_20260916.md` **§四(P0-1/2/3)** + **§五** > - 三条 P0 的设计推演(**实体在这里**):`覆盖网络_问题逐条推演与解决方案_20260916.md` **§A1 / §A2 / §A3** > - 三概念拆分(接入 / 成员 / 可见):`覆盖网络_骨干层方案_20260916.md` **§3.1** > - ① 的落地事实(本单的**起点**,含回滚表):`交接单_relay落地R2-R4_20260916.md` **§9 / §10 / §11** --- ## 0. 判定:这一单在解决什么 **主线 ① 已完工**(`会合中继拆分 S0–S4` + `R5 会合可换机`,见上表第 4 行那份单子)⇒ 机制层已经"能用"。 **但机制层有一个结构性缺口**:relay 把**平台自己的 Worker 隧道**和**未来的用户设备**放进**同一个扁平 `hostId` 命名空间** (`src/net/relay/server.ts` 的 `hostId` 全局唯一、无网络维度;全仓 `grep -ri networkId|tailnet` = **0 命中**)。 ⇒ 今天只有 1 个用户、1 个网络,问题不显形;**一进第二类节点就会变成"一张巨网 + 靠 ACL 兜"**, 与项目"**权限只准收窄**"直接冲突(这正是 `清单 §四 P0-1` 的判定)。 **本单要做的三件事**(= `清单 §四` 的 P0-1/2/3,别扩大): 1. **P0-1 网抽象**:`network_id` 成为**结构性隔离**的维度(每用户一张网,不是一张巨网 + ACL)。 2. **P0-2 首次入网引导**:三级引导链(内置种子 → 签名目录 → 离线降级),且**引导地址可在线轮换**。 3. **P0-3 地址规划与名字解析**:逻辑名 `/` + 按网络授权。 ⛔ **本单明确不做**:应用层 / 房间层 / presence(`清单 §五` 排在第 **7** 步)、内容分发、游戏服、 虚拟网卡(L3)—— 理由见 §3 的 **D1**。 --- ## 1. 只读前置(开工前逐条跑,全绿才动代码) ```bash # 1) 指纹(不符 ⇒ 停) tail -n +4 交接单_网抽象与地址规划R6_20260916.md | md5sum # 2) 抢全局执行锁(抢不到 = 有会话在跑 = 停手,R9) bash "D:/github/dsh_shenxian/dsh-server-docs/scripts/handoff-guard.sh" --claim-exec "<你的会话名>" # 3) 状态快照(1 次调用顶十几轮探索;⛔ 跑它之前别 Glob/Grep 全库) "E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" \ "E:/ProgramData/AI技能/aliyun-dsh-server/state.py" # 4) 起点事实复核(① 的终态必须仍然成立,任一条不符 ⇒ 先查为什么,别在本单里顺手修) ssh root@47.77.182.89 'systemctl is-active dshs dshs-relay; curl -s -m 8 127.0.0.1:20080/status | python3 -c " import json,sys; d=json.load(sys.stdin) print(\"sessions:\", [(s[\"hostId\"], s[\"ports\"]) for s in d.get(\"sessions\", [])]) print(\"dialers:\", d.get(\"dialers\"))"' # 5) 改哪些文件的现状(本单要碰的 3 个接缝) grep -n "hostId" D:/github/dsh_shenxian/src/net/relay/server.ts | head -20 # 扁平命名空间的现场 grep -n "relayUrl\|relayStatusUrl\|relayDialHost" D:/github/dsh_shenxian/src/config.ts ``` --- ## 2. 已定项(**我的技术决策 —— 已定,可推翻**;⛔ 不要再拿去问用户) ### D1 · ② 阶段**不引入虚拟网卡 / L3 地址** 沿用 `hostId` 命名 + **wss mux 流转发**(R0–R5 已跑通、零新增公网口、**不需要管理员权限**)。 - **为什么**:L3(TUN)会立刻带进三个我们自己文档已经点名的坑 —— ① 虚拟网卡驱动**需要管理员权限**(`清单 §四 P1` 明列为 **R5 门禁**); ② **中国移动等运营商的大内网正是 `100.64.0.0/10`**,而我们设备池里有 **200 台 CGNAT + 150 台移动网** (`§A3` 推演)⇒ 覆盖网地址若落进该段,会出现**宿主路由冲突**、表现是"**部分节点时通时不通、极难排查**"; ③ MagicDNS / split DNS 有**覆盖用户系统 DNS** 的风险(`§A3`)。 - ⇒ **本单不碰 `100.64.0.0/10`、不自建 DNS、不改用户解析**。P0-3 在本阶段收敛为"**逻辑名 + 授权**"; **物理地址段问题整体推迟到将来的 L3 专项**,前置约束已在 §6 落笔(别丢)。 ### D2 · 网络标识用**租户维度的显式列**,不新造体系 - `network_id` 的取值:**运维网固定 `ops`**(47 / 106 / 未来的中继与骨干);**用户网 = `u:`**(该用户全部设备)。 - 节点逻辑名 = **`/`**;relay 侧**按网络授权**(`ops` 只拨 `ops`;`u:X` 只拨 `u:X`)。 - **结构性隔离 > 策略性隔离**(`§A1` 的关键判断):**每用户一张网**,不是"一张巨网 + ACL"。理由:写错一条 ACL 就泄露,而结构性隔离**不可能越界**。 - ⚠️ `§A1` 明写「**要在第一次落地时就分开,不能等**」⇒ 本单必须**真落地**,只写文档不算完成。 - ⚠️ 与既有判据一致:**会合/中继可多实例(数据面),成员资格只能由控制面签发**(`骨干层方案 §3.2`)。 ### D3 · 引导走三级链,**种子复用现有域名**(不新增域名成本) - ① **内置种子**:常量位 = `https://alotbuy.com/dshs-relay`(已持证书);**同时留出第二地域的常量位但不填**。 - ② **签名目录**:控制面下发(签名 + 版本号),客户端缓存,按 `update_frequency` 刷新。 - ③ **离线降级**:缓存过期仍可用(只影响**新节点加入**,不影响**已建连接**)。 - 🔑 **`§A2` 的关键设计要求(不做会成灾)**:目录里**必须**带 `bootstrap[]` 字段, **引导地址要能通过已建立的连接在线下发** —— 否则将来换域名/换机器 = **所有客户端必须升级重装**。 ### D4 · 只做**逻辑名**,不做 DNS / 不下发对端清单 - 连接授权按 `network_id` 判;**接入与可见解耦**(`骨干层方案 §3.1`:可借道转发,但不暴露对端清单)。 --- ## 3. 范围 **会碰**(预计 6–9 个文件,全部 ≤10 ⇒ 不受 R7 批量写入约束): `src/config.ts`(+ `overlayBootstrapSeeds` / `overlayNetworkId`)|`src/net/relay/wire.ts`(HELLO 加 `network`)| `src/net/relay/server.ts`(按网络授权 + 目录签名)|`src/net/relay/client.ts` / `dialer.ts`(带上自己的网络)| `src/web/server.ts`(`/dshs-overlay/bootstrap` 只读端点)|DB 迁移(`dsh_hosts` 加 `network_id`)|新增 `test/overlay-network.test.mjs`。 **⛔ 不许碰**:`@deepseek-ai/dsh` 主程序与缓存(R2)|client bundle 的 `exports.default`(R3)| relay 的既有帧语义(只能**加**帧,不能改旧帧)|`32023` / SSH 版中继(已判「不做」)| `100.64.0.0/10`(D1)|应用层 / 房间层(§0)。 --- ## 4. 步骤(每步单独可回滚,**做完一步就停下来报一次**) ### Step 1 · P0-1 网抽象(`network_id` 落地) 1. `dsh_hosts` 加列 `network_id text not null default 'ops'`(**运维网是现网事实** ⇒ 存量行天然正确)。 2. relay `HELLO` 帧加 `network` 字段(**只加字段**);relay 侧由 `dialers: Set` 升级为 `dialers: Map>` 语义(**权限只收窄**:默认拒绝,命中才放行)。 3. Manager 的 `DSHS_RELAY_DIALERS` 由「扁平 hostId 列表」改为「`network:hostId` 列表」,**旧值照旧可用**(过渡期不破坏现网)。 4. **判据(这一步的核心)**:`ops/manager` 仍能拨 `ops/w-106`;构造一个 `u:test-network` 的注册方, 断言它**拨不动** `ops/w-106`(`dialer-not-in-network`)。 ⇒ **把"不能到达"变成可回归断言**(这就是 `§A1` 说的"ACL tests 进 CI"的等价物)。 ### Step 2 · P0-2 引导(三级链) 1. 新增**只读**端点 `GET /dshs-overlay/bootstrap`:返回 `{version, relays[], bootstrap[], sig}`, `bootstrap[]` = **可轮换的引导地址清单**(D3 的 🔑)。 2. `config.ts` 加 `overlayBootstrapSeeds`(默认 `['https://alotbuy.com/dshs-relay']`,第二地域留空)。 3. 客户端取址顺序:**env 显式 > 缓存目录(未过期)> 内置种子**;任一成功即写缓存。 4. **判据**:清空 env + 清缓存启动 ⇒ 仍能起来(走种子);**改目录里的 `bootstrap[]`** ⇒ 新会话读到新值(**不重装、不升级**);签名不对 ⇒ **失败关闭**(不得静默接受)。 ### Step 3 · P0-3 名字解析与授权 1. 节点逻辑名统一为 `/`(`place/resolve` 的唯一入口各改一处,别在调用方拼)。 2. 跨网络访问**在 relay / 控制面被拒**(⛔ 不许靠客户端自觉)。 3. **判据**:`u:A` 的节点看不到、也到不了 `u:B` 的节点;`ops` 节点对用户网络**默认不可见**。 --- ## 5. 验收(命令级,缺一条不算完成) | # | 判据 | 期望 | |---|---|---| | 1 | `node --test test/relay.test.mjs test/remote-spawner.test.mjs test/remote-user-fs.test.mjs test/overlay-network.test.mjs` | 全绿(**含 ① 的 T18/T19 与 A1 的 U1–U8**,⛔ 一条都不许退化) | | 2 | `node scripts/check-layering.mjs` | **无新增违规**(基线 5 条) | | 3 | `ops/manager` → `ops/w-106` 控制面 + 实例面 | 全通(R5 终态不退化) | | 4 | `u:test-network` → `ops/w-106` | **被拒**,且拒绝点是 relay(有日志),不是客户端 | | 5 | 清 env + 清缓存冷启动 | 经**内置种子**取到目录并连上 | | 6 | 目录里 `bootstrap[]` 改值 | 新会话读到新值(**不重装**) | | 7 | 门户 + 实例页 | 仍 `200`(用户可见面不受影响) | | 8 | 公网暴露面 | **零新增**(`ss -lntH` 对比基线;relay 仍只绑 `127.0.0.1:20080`) | --- ## 6. 回滚(分层,任一层可单独退) | 层 | 动作 | 影响 | |---|---|---| | 代码 | `scp` 回 `/opt/dshs/lib-bak-*` 对应文件 ⇒ `systemctl restart dshs` | 回到本单之前 | | relay | `DSHS_RELAY_DIALERS` 改回扁平 `hostId` 列表 ⇒ `restart dshs-relay` | 网络维度失效,**行为回到 ① 终态** | | DB | `dsh_hosts.network_id` 列**保留但不再读**(⛔ **不要 drop**:现网列删除不可逆) | 无影响 | | 端点 | `/dshs-overlay/bootstrap` 直接下线路由 | 客户端回落到 env 取址(= 今天行为) | > 📌 **L3 专项的前置约束(将来谁做谁先读)**:① 虚拟网卡需管理员权限 ⇒ **命中 R5,先出权限影响评估**; > ② **避开 `100.64.0.0/10`**(中国移动大内网就是它,本池 200 台 CGNAT + 150 台移动网); > ③ 避开 `10/8` · `192.168/16` · `172.16/12`;④ `base_domain` 必须与 `server_url` 域名**不同**; > ⑤ 默认**不覆盖**用户系统 DNS(`magic_dns: false` + 客户端 `--accept-dns=false`)。 --- ## 7. 残留与已知项(本单**不**解决,别顺手做 —— R7) - `state.py` 的锁判据**已于 2026-09-16 修**(原把 `.exec-lock` 目录当文件读 ⇒ 恒报"空闲")—— 复验:持锁时运行必须报**占用**。 - 腾讯云安全组 `32022` 放行规则仍在(本机已无监听)⇒ 需控制台凭据,**不属本单**。 - `覆盖网络_瓶颈落地方案 §3` 标题的 `10.8 GB` 笔误(实为 **10.8 MB/台**)⇒ `清单 §四 P2`,**不属本单**。 --- ## 8. 回报格式(每步做完照这个写,别自由发挥) ``` ## Step N · - 改动:<文件:行 级清单>(新增/修改分开写) - 判据:<命令> ⇒ <实测输出原文> - 未过 / 存疑:<逐条,没有就写"无"> - 回滚:<本步的退法,一句话> - 纪律:未 commit / 未 push;锁 <持/放>;工具调用 N 次 ``` --- ## 9. 纪律(沿用本线惯例) - ⛔ **不 commit、不 push**(未获授权);改完只铺 `47` 的 `/opt/dshs/lib/` + 重启 `dshs`。 - 动手前抢全局执行锁,**做完立刻释放**(带锁结束回合 = 把所有人挡在门外)。 - 每步 **只做一件事**;发现的额外问题**先报告后动手**(R7)。 - ⛔ **不要回头问"要不要部署"** —— 部署是本 lane 内的执行细节,直接做,动手前一句话说明。 - ⛔ 完成任一段后**不要**把"下一棒"写成应用层 / presence(`清单 §五` 排在第 7 步)。