Files
dsh_ai1net_server/归档/交接单-20260924-归档/交接单_网抽象与地址规划R6_20260916.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

204 lines
14 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.
# 交接单 · 覆盖网络主线 ②「网抽象 + 地址规划 + 引导」(**执行会话专用** · 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) | 逻辑名 `<network>/<hostId>` 成为 `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 地址规划与名字解析**:逻辑名 `<network>/<host>` + 按网络授权。
⛔ **本单明确不做**:应用层 / 房间层 / 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 [email protected] '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:<userId>`**(该用户全部设备)。
- 节点逻辑名 = **`<network_id>/<hostId>`**;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<string>` 升级为
`dialers: Map<networkId, Set<hostId>>` 语义(**权限只收窄**:默认拒绝,命中才放行)。
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. 节点逻辑名统一为 `<network_id>/<hostId>`(`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 · <P0-x 名称>
- 改动:<文件:行 级清单>(新增/修改分开写)
- 判据:<命令> ⇒ <实测输出原文>
- 未过 / 存疑:<逐条,没有就写"无">
- 回滚:<本步的退法,一句话>
- 纪律:未 commit / 未 push;锁 <持/放>;工具调用 N 次
```
---
## 9. 纪律(沿用本线惯例)
- ⛔ **不 commit、不 push**(未获授权);改完只铺 `47` 的 `/opt/dshs/lib/` + 重启 `dshs`。
- 动手前抢全局执行锁,**做完立刻释放**(带锁结束回合 = 把所有人挡在门外)。
- 每步 **只做一件事**;发现的额外问题**先报告后动手**(R7)。
- ⛔ **不要回头问"要不要部署"** —— 部署是本 lane 内的执行细节,直接做,动手前一句话说明。
- ⛔ 完成任一段后**不要**把"下一棒"写成应用层 / presence(`清单 §五` 排在第 7 步)。