交接单按归属约定(正文落文档库、工作区只放指针)归档至 归档/交接单-20260924-归档/,含逐件判定 README: - 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26) - 4 件主题已被覆盖网络线入口汇总 - 7 件历史接续包/规划件 CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、 工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
14 KiB
交接单 · 覆盖网络主线 ②「网抽象 + 地址规划 + 引导」(执行会话专用 · 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,别扩大):
- P0-1 网抽象:
network_id成为结构性隔离的维度(每用户一张网,不是一张巨网 + ACL)。 - P0-2 首次入网引导:三级引导链(内置种子 → 签名目录 → 离线降级),且引导地址可在线轮换。
- P0-3 地址规划与名字解析:逻辑名
<network>/<host>+ 按网络授权。
⛔ 本单明确不做:应用层 / 房间层 / presence(清单 §五 排在第 7 步)、内容分发、游戏服、
虚拟网卡(L3)—— 理由见 §3 的 D1。
1. 只读前置(开工前逐条跑,全绿才动代码)
# 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 落地)
dsh_hosts加列network_id text not null default 'ops'(运维网是现网事实 ⇒ 存量行天然正确)。- relay
HELLO帧加network字段(只加字段);relay 侧由dialers: Set<string>升级为dialers: Map<networkId, Set<hostId>>语义(权限只收窄:默认拒绝,命中才放行)。 - Manager 的
DSHS_RELAY_DIALERS由「扁平 hostId 列表」改为「network:hostId列表」,旧值照旧可用(过渡期不破坏现网)。 - 判据(这一步的核心):
ops/manager仍能拨ops/w-106;构造一个u:test-network的注册方, 断言它拨不动ops/w-106(dialer-not-in-network)。 ⇒ 把"不能到达"变成可回归断言(这就是§A1说的"ACL tests 进 CI"的等价物)。
Step 2 · P0-2 引导(三级链)
- 新增只读端点
GET /dshs-overlay/bootstrap:返回{version, relays[], bootstrap[], sig},bootstrap[]= 可轮换的引导地址清单(D3 的 🔑)。 config.ts加overlayBootstrapSeeds(默认['https://alotbuy.com/dshs-relay'],第二地域留空)。- 客户端取址顺序:env 显式 > 缓存目录(未过期)> 内置种子;任一成功即写缓存。
- 判据:清空 env + 清缓存启动 ⇒ 仍能起来(走种子);改目录里的
bootstrap[]⇒ 新会话读到新值(不重装、不升级);签名不对 ⇒ 失败关闭(不得静默接受)。
Step 3 · P0-3 名字解析与授权
- 节点逻辑名统一为
<network_id>/<hostId>(place/resolve的唯一入口各改一处,别在调用方拼)。 - 跨网络访问在 relay / 控制面被拒(⛔ 不许靠客户端自觉)。
- 判据:
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 步)。