交接单按归属约定(正文落文档库、工作区只放指针)归档至 归档/交接单-20260924-归档/,含逐件判定 README: - 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26) - 4 件主题已被覆盖网络线入口汇总 - 7 件历史接续包/规划件 CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、 工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
204 lines
14 KiB
Markdown
204 lines
14 KiB
Markdown
# 交接单 · 覆盖网络主线 ②「网抽象 + 地址规划 + 引导」(**执行会话专用** · 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 步)。
|