起因:用户 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 代码面)。
24 KiB
会合 / 中继从 Manager 拆分 — 只读取证 + 可执行改造方案(2026-09-16)
🟢 状态:已完成使命 · 仅存档(2026-09-17 16:4x 由序 ⑯ 规划棒复核后标注) 复核结论 = 判「不做」:本方案 §4 的四个耦合点(C1–C4)与两步改造(S3 / S4)已逐条被后续序次覆盖,未覆盖部分 = 无。 C1 → 序② P0-2(
DSHS_RENDEZVOUS_URL优先、DSHS_TUNNEL_TARGET降兜底,src/config.ts:485-490)|C2 → R5(Manager 只拨出、落点搬到本机回环池127.0.0.1:25000+,src/config.ts:498-500)|C3 → S2/P2(dsh_hosts.via+RendezvousRegistry)|C4 → 本方案 §8.1 实测前提不成立|S3 → 本方案 §9.1 自我证伪后改「实例端口区间隔离」|S4 → R2(dshs-relay独立单元)+ 序⑥ S8(106 升格第二中继)+ 序⑦(多实例切流实测)。 ⛔ 勿再按 §4 的 S0–S4 开工。复核证据 = 工作区根交接单_在册收尾_20260917.md§0.1(逐条对照表)。 ⚠️ 正文不改(历史档案属性)—— 上表即勘误入口。
来源:
接续入口_覆盖网络线_20260916.md §2 第 2 条(第一优先 · 真活)。 本步只读:⛔ 未改代码、未重启、未动服务器。全部结论来自D:\github\dsh_shenxian工作树(HEAD4e3a1a4)的源码静态阅读。 为什么是前置:中继绑在 Manager 上 ⇒ 多区域 / 多中心 / 骨干层全部做不了。这一刀不切,后面九大瓶颈方案里的中继容量、45% 中继规划都无从落地。
0. 结论先行
| 判定 | 内容 |
|---|---|
| 现状 | 「会合 + 中继」不是一个组件,而是 Manager 主机上 sshd 的副作用 —— 会合点 = 47.77.182.89:32022,中继落点 = Manager 的 127.0.0.1。 |
| 真问题 | 功能能跑(档案 103 已判"现有架构给出 80% 形状")。⚠️ 卡点是"耦合写死在 4 处",不是"协议不对"。 |
| 因此第一步不是换协议 | ⛔ 不要一上来换 WireGuard / TURN。先把会合地址与中继落点抽成配置与接口,让 SSH 隧道退化成"第一个可替换实现"。每一步独立可验、独立可回滚。 |
| 分层口径(已定,勿破) | 会合 = 位置视图(可多实例、可无状态);中继 = 数据面(可多实例);归属 / 租约 / 骨干资格 = 只能控制面写(本方案不碰)。 |
1. 取证:现状到底长什么样
1.1 链路(跨机形态,实测形态)
Worker 106 Manager 47
┌────────────────────────────┐ ┌──────────────────────────────┐
│ dshs-worker.service │ │ dshs.service (deployMode= │
│ WorkerAgent :19000 (lo) │ │ cluster) │
│ └─ SshTunnel ──── ssh -R ─┼───────────────▶│ sshd :32022 │
│ 每实例 :PORT (lo) │ ControlMaster │ └─ 反转到 127.0.0.1:PORT │
└────────────────────────────┘ │ RemoteSpawner → agentUrl │
│ = http://127.0.0.1:19000 │
┌─────────────────────────▶ proxy.ts → out.host = 127.0.0.1:PORT
│ │ PostgreSQL :15432 (静态转发)│
│ └──────────────────────────────┘
用户浏览器 → alotbuy.com → 47
1.2 关键代码位(行号 = 本次阅读的 HEAD)
| 角色 | 文件:行 | 事实 |
|---|---|---|
| 隧道实现 | src/worker/tunnel.ts:96-120 |
ssh -M -N -f -S <ctl> -i <key> -R <port>:127.0.0.1:<port> —— 两端同端口号 |
| 隧道加/减 | src/worker/tunnel.ts:147 / :163 |
ssh -O forward / -O cancel,靠 ControlMaster 复用同一条长连接 |
| 隧道自愈 | src/worker/agent.ts:183-219 |
healTunnel() 20 s 定时器 + 心跳里顺带 -O check |
| Worker 侧配置 | scripts/switch-C-worker.sh:19-20 |
[email protected]:32022、DSHS_TUNNEL_IDENTITY=/root/.ssh/tunnel_ed25519 |
| 隧道开关 | src/worker/agent.ts:154 |
tunnelTarget === '' ⇒ 完全不建隧道(同机形态零影响) |
| Manager 侧配置 | src/config.ts:350-354 |
DSHS_CLUSTER_HOST_ID / _AGENT_URL / _AGENT_TOKEN / _INSTANCE_HOST |
| 装配 | src/web/server.ts:256 |
hostsProvider 把 dsh_hosts.endpoint 直接当 agentUrl,instanceHost 取全局 clusterInstanceHost |
| 访问面 | src/supervisor/proxy.ts:109 |
out.host = 127.0.0.1:<port> —— 代理写死 loopback |
| 数据面契约 | src/supervisor/remote-spawner.ts:24-34 |
ClusterHost{hostId, agentUrl, token, instanceHost} —— 没有"经谁可达"这个字段 |
| 注册 | src/web/routes/admin.ts:184-203 |
POST /api/admin/hosts(join 脚本调用,幂等) |
| 心跳 | src/worker/agent.ts:255-262 |
/healthz 回 {ok, hostId, instances, tunnel:{ready,ports}} |
| 选机 | src/web/server.ts:300-318 |
粘性优先 → 容量准入(capacityMb<=0 不限 / -1 禁用) |
2. 四个耦合点(这是本方案的靶子)
| # | 耦合点 | 具体表现 | 不拆的后果 |
|---|---|---|---|
| C1 | 会合地址硬编码在 Worker 的 env | [email protected]:32022 写死在 switch-C-worker.sh + /etc/dshs-worker.env |
会合点换机 / 加第二台 ⇒ 每台 Worker 都要改 env 重启;会合点不能独立部署 |
| C2 | 中继落点命名空间 = Manager 的 loopback(且两端同端口号) | tunnel.ts 用 -R <port>:127.0.0.1:<port>;proxy.ts:109 写死 127.0.0.1 |
① 端口空间与 Manager 全局共享(冲突面 = 所有 Worker 实例端口并集)② 第三台机器无法直达 Worker(只有 Manager loopback 有那个端口)③ 中继不可多实例(多实例无从分配端口) |
| C3 | 可达性登记把"经谁中转"磨掉了 | dsh_hosts.endpoint 存的是 127.0.0.1:19000 —— 语义上不是 worker 的地址,是 Manager 本地一个转发口;hostsProvider 原样当 agentUrl |
换会合/中继组件时,表里无法表达「via(经哪个中继)+ 真实可达地址」⇒ 必须改表,越晚改代价越大 |
| C4 | 控制面 PostgreSQL 也走同一条隧道(静态转发) | DSHS_TUNNEL_STATIC_PORTS(src/worker/agent.ts:157),设计上含控制面 PG |
会合组件一换,控制面库的连接路径一起断 ⇒ 回滚面比看上去大 |
3. 目标形状(分层,且不新建权威状态)
| 组件 | 职责 | 可以多实例? | 权威状态 |
|---|---|---|---|
| 会合 (rendezvous) | 收 Worker 注册、回"该拨谁"、下发中继分配;不承载数据面流量 | ✅ 无状态可复制 | ❌ 只有位置视图 |
| 中继 (relay) | 数据面:Worker 拨它 → Manager / 其他节点经它到 Worker | ✅ | ❌ |
| 控制面 (Manager) | 归属 / 租约 / 骨干资格 / 容量准入 | ❌ 单点 | ✅ 唯一写入者 |
硬约束:会合与中继不得写入 dsh_instances.host_id / epoch / 骨干资格 —— 否则就是双写脑裂(与 集群化改造方案 §1.3 一致)。
4. 分步方案(S0–S4 必做 · S5 后续)
S0 — 抽接口(零行为变化)
- 改哪些文件
- 新增
src/net/rendezvous.ts:interface Rendezvous { id: string; dialTarget(): string; resolve(hostId): Promise<Reachability|undefined> } - 新增
src/net/reachability.ts:interface Reachability { hostId: string; via: string; address: string; scheme: 'http'|'https' } - 改
src/supervisor/remote-spawner.ts:24-34:ClusterHost增reachability?: Reachability,agentUrl降级为派生 getter(保向后兼容)
- 新增
- 接口怎么切:
RemoteSpawner/RemoteUserFs/proxy.ts三处都只消费Reachability,不再各自拼http://127.0.0.1:<port>。 - 怎么验:
npm run build通过 +node scripts/verify-cluster-cross.mjs全绿(行为零变化是唯一验收标准)。 - 怎么回滚:纯新增文件 + 可选字段,
git revert <commit>即可。
S1 — 会合地址出 env(打掉 C1)
- 改哪些文件:
src/worker/agent.ts:154-172、src/worker/tunnel.ts:26-65、src/config.ts:350-354 - 接口怎么切:新增
DSHS_RENDEZVOUS_URL(ssh://[email protected]:32022或未来的https://...);DSHS_TUNNEL_TARGET保留为兜底(未设DSHS_RENDEZVOUS_URL时走它)⇒ 两台机器可分先后改。 - 怎么验:改一台 Worker 的 env →
systemctl restart dshs-worker→curl -H <token> http://127.0.0.1:19000/healthz里tunnel.ready === true;Manager 侧verify-cluster-cross.mjs通过。 - 怎么回滚:删掉新 env 即在走旧路径(代码同时保留两条),不需回滚代码。
S2 — dsh_hosts 增 via 列(打掉 C3)
- 改哪些文件:
src/db/adapter.ts:115-121、src/db/pg.ts:602-646、src/db/repo.ts:601-625、src/db/sqlite.ts:343-366、src/web/server.ts:256-266、src/web/routes/admin.ts:184-203 - 接口怎么切
- 迁移:
ALTER TABLE dsh_hosts ADD COLUMN via text NOT NULL DEFAULT 'manager-ssh'; ADD COLUMN address text; - 回填:把现
endpoint里的127.0.0.1:19000拆成via='manager-ssh'+address='127.0.0.1:19000'(endpoint列保留不删) hostsProvider:先读via→ 选对应Rendezvous实现 → 解析成Reachability;via未设时回退endpoint原语义。
- 迁移:
- 怎么验:脚本断言「迁移前
agentUrl」与「迁移后resolve()结果」逐条相等(这就是验收判据,不靠肉眼)。 - 怎么回滚:列有默认值 ⇒ 旧代码读
endpoint完全不受影响,列可留着不删。
S3 — 中继落点命名空间可配(打掉 C2 的一半)
- 改哪些文件:
src/worker/tunnel.ts:142-160、src/supervisor/proxy.ts:109 - 接口怎么切:新增
DSHS_RELAY_LOCAL_NAMESPACE(默认127.0.0.1)⇒ 允许每条转发落在独立回环地址(127.0.0.2、127.0.0.3…),由会合下发端口而非"两端同号"。 - ⚠️ 同号是
endpointFor免映射表的前提 ⇒ 改不同号必须同时给ClusterHost加portMap。所以本步验收必须含两条端到端,不能只看健康检查。 - 怎么验:①
verify-cluster-cross.mjs全绿 ② 跨机实例页能打开 ③ 跨机工作区文件能读能写(文件面与实例面同一份路由,见server.ts:274-288的注释)。 - 怎么回滚:不设该 env 即回到"同号"原状。
S4 — 中继独立成单元 + 会合可换机(打掉 C2 的另一半 + C1 的尾巴)
- 做什么:把"接收 Worker 反拨"这半边从 Manager 的 sshd 里搬出来 → 独立 systemd 单元
dshs-relay.service(第一版仍用 sshd,但独立端口 + 独立authorized_keys+ 独立系统账号)。Manager 通过via指过去。 - 为什么这样切:不换协议、只换绑定关系 ⇒ 已跑通的隧道逻辑 0 改动,风险面最小;换来的是"中继可换机、可多实例、Manager 不再兼任会合"。
- 怎么验(三条,缺一不可)
- 单中继实例下全链路通(实例面 + 文件面)
- 停掉中继 ⇒ 同机形态实例仍可用、跨机代理失败但不崩(验证"中继非必需依赖")
- 会合指向第二个中继实例,新 Worker 注册后能被 Manager 正确解析(验证多实例)
- 怎么回滚:
dsh_hosts.via改回manager-ssh+systemctl disable --now dshs-relay。
S5 — 443/TCP 兜底通道(后续,非本方案前置)
已定口径(异构设计:企业 / 校园网常封 UDP)。落地时挂到 relay 上做传输变体,不动会合接口。
5. 待现场复核(本轮只读,未在服务器执行任何命令)
| # | 待复核项 | 为什么重要 | 若为否的影响 |
|---|---|---|---|
| 1 | 106 上 DSHS_TUNNEL_STATIC_PORTS 是否真含控制面 PG 端口 |
决定 C4 是否成立、回滚面是否含 DB | 仅影响回滚清单;S2/S3 不变 |
| 2 | dsh_hosts 里 w-106 那条 endpoint 的实际值 |
决定 S2 回填脚本的取值 | 需先取真实值再写迁移 |
下令复核的命令(只读,未执行)
# 远程服务器 106
systemctl show dshs-worker -p Environment
# 远程服务器 47
sudo -u postgres psql -d dshs -c "SELECT id, endpoint, status, capacity_mb FROM dsh_hosts ORDER BY id;"
6. 风险与反模式
| 风险 | 处置 |
|---|---|
| 把会合做成"藏了权威状态的隐形控制面" | 会合只读位置视图;归属写入路径一律仍走 db.claimInstance(epoch+1 fencing 不变) |
| 一步到位换传输协议 | ⛔ 反模式。S0–S4 全程不换协议,只换绑定与寻址 |
| S3 只看健康检查就收工 | ⛔ 端口映射一改,endpointFor 的隐含前提就变 ⇒ 必须跑通"实例页 + 文件读写"两条端到端 |
| 先改表再改代码(或反之) | via 列有默认值 ⇒ 顺序必须是先加列 → 再改代码 → 最后回填,任一步中断都不崩 |
7. 交付边界(本步)
- ✅ 产出:本方案(改哪些文件 / 接口怎么切 / 分几步 / 每步怎么验 / 怎么回滚)。
- ⛔ 未做:未改任何代码、未重启任何服务、未动 47 / 106。
- 下一步:按 S0 开工即可 —— 纯新增文件 + 类型扩展,零行为变化,是整条链里风险最低的入口。
8. 现场复核结论 + 方案勘误 + S0 执行记录(2026-09-16 13:1x 追加)
本节的复核命令全部只读(
systemctl show/cat/ss/psql SELECT),未改服务器任何状态。 执行环境:本机 →[email protected](22)+test106(~/.ssh/id_ed25519_test106)。
8.1 §5 两项待复核 —— 已复核
| # | 项 | 实测结论 |
|---|---|---|
| 1 | 106 是否真含控制面 PG 的静态转发 | ❌ 不含。106 /etc/dshs-worker.env 没有 DSHS_TUNNEL_STATIC_PORTS;47 的 worker env 也没有(且 47 根本没设 DSHS_TUNNEL_TARGET ⇒ 同机形态完全不建隧道)⇒ staticPorts 只有 agent 自身端口 ⇒ C4 在本形态下不成立,回滚面不含 DB |
| 2 | dsh_hosts 里各条 endpoint 的实际值 |
w-106 = http://127.0.0.1:19000|w-47 = http://127.0.0.1:19100 |
47 端口面实测(ss -lntp):0.0.0.0:32022 sshd(会合点)|127.0.0.1:19000 sshd=106 的隧道落点|127.0.0.1:43937 sshd=106 上那个实例的端口,同号反转|127.0.0.1:19100 node=w-47 的 agent|127.0.0.1:15432 postgres。
⇒ §2 的 C2「中继落点 = Manager loopback + 两端同号」实测成立;/healthz 亦证实 w-106 的 tunnel.ports = [19000, 43937]。
8.2 三处必须勘误(照字面做会出事故)
① 🔴 proxy.ts:109 不是中继连接目标 —— ⛔ S3 绝不能改它。
buildUpstreamHeaders() 里的 out.host = \127.0.0.1:${port}`是**发给上游 dsh 的 HTTPHost头**,源码注释已写明原因:dsh 的/api信任栅栏要求 Host 是 loopback(或--trusted-host授权项),否则**每条/api 调用都 403**。 真正的连接目标在 **proxy.ts:138-139** 的 { host: endpoint.host, port: endpoint.port }(来自 endpointFor)。 ⇒ S3 做「中继落点命名空间可配」时,**只动连接地址那一侧**;改动 proxy.ts:109会让实例(不只跨机)的/api` 全量 403。
② 🔴 两条 endpoint 字符串同形、语义不同 ⇒ S2 回填不能一刀切 via='manager-ssh'。
w-47→127.0.0.1:19100是 node 自己监听的端口(同机直连)⇒via = 'local'w-106→127.0.0.1:19000是 47 上 sshd 的隧道落点 ⇒via = 'manager-ssh'
§4 S2 原文只写了后者(把 127.0.0.1:19000 拆成 manager-ssh)⇒ 按行回填别按字面。
③ ⚠️ 47 的 cluster 配置在 systemd drop-in,不在 /etc/dshs.env。
实际位置 = /etc/systemd/system/dshs.service.d/cluster.conf(DSHS_DEPLOY_MODE=cluster / DSHS_DB_URL / DSHS_CLUSTER_* 十条;/etc/dshs.env 里没有 cluster 段)。§5 的回滚命令(删 drop-in → daemon-reload → restart dshs)正确,无需修改,只是换文件位置要照此。
8.3 S0 执行记录(已落地本机 · 未部署)
| 文件 | 改动 |
|---|---|
src/net/reachability.ts |
新增:Reachability + agentBaseUrl() + agentBaseUrlOf()(全仓唯一取址入口) + parseReachability()/toEndpoint()(互逆)+ VIA_LOCAL / VIA_MANAGER_SSH |
src/net/rendezvous.ts |
新增:Rendezvous 接口 + LocalRendezvous + ManagerSshRendezvous + RendezvousRegistry(S2 才接调用点,本轮不消费) |
src/supervisor/remote-spawner.ts |
ClusterHost.agentUrl → 可选、新增 reachability?;构造期归一化移除(移到取址处、幂等);call() / touch() 改走 agentBaseUrlOf() |
src/web/server.ts |
两处 agentFor 改 agentBaseUrlOf(h) —— 类型收紧后必须一起改,方案 §4 S0 未列出 |
test/reachability.test.mjs |
新增 8 条断言:把现网真实两行 endpoint 写死为判据 ⇒「解析前后 agent 基址逐字相等」不靠肉眼 |
package.json |
test / verify 挂上新测试 |
- 验收实测:
npm run buildRC=0 |npm test44 项 / 0 失败(新增 8 项全绿)。lib/产物里已无host.agentUrl直接拼接。 - 为什么未部署:S0 零行为变化 ⇒ 部署没有任何可观察收益,却要
restart dshs打断在线实例 ⇒ 与 S1 合并上线(一次重启换一个真实变化)。⛔ 这不是"漏了部署"。 - 有意未收敛的两处(避免本轮动 5 个文件,留给 S2 与
via列一起改): ①leased-spawner.ts的agentFor契约仍是{agentUrl, token}; ②remote-user-fs.ts:57/73自己又做了一次.replace(/\/$/,'')。 两处的取值都已来自经过agentBaseUrlOf()的agentFor⇒ 行为正确,只是还没换成Reachability形态。 - 回滚:
git checkout -- src/supervisor/remote-spawner.ts src/web/server.ts package.json+ 删src/net/+test/reachability.test.mjs(纯新增 + 可选字段,无数据迁移)。 - 下一步:S1(会合地址出 env,打掉 C1)—— 新增
DSHS_RENDEZVOUS_URL、DSHS_TUNNEL_TARGET降为兜底,两台机器可分先后改。
9. 第二轮勘误(2026-09-16 15:2x · 本机 + 双机只读实测)
本轮复核了 §4 的可执行性,结论:S3 的机制不可用,须换做法;另外 S1 的部署面在本方案里写漏了 47。 操作层细节(命令 / 验收 / 回滚 / 清理)已落
交接单_覆盖网络落地执行_20260916.mdv2 §0 + §3,本节只记设计层的判断。
9.1 🔴 S3 的 DSHS_RELAY_LOCAL_NAMESPACE(回环别名)实测无效 —— 机制换成「端口区间隔离」
实测(本机 → 106 → 47,测完已清理):自 106 发起
ssh -R 127.0.0.2:19999:127.0.0.1:19000 -o ExitOnForwardFailure=yes [email protected]:32022
⇒ 47 上落点实际是 127.0.0.1:19999(另有 [::1]:19999),127.0.0.2 被静默改写;ssh 侧零报错(ExitOnForwardFailure 未触发、日志为空)。
根因:47 sshd -T ⇒ gatewayports no(OpenSSH 默认)⇒ sshd 把远端转发强制绑到回环,客户端指定的绑定地址被丢弃。
⇒ §4 S3 原设计(127.0.0.2、127.0.0.3… 独立回环别名)作废,改做 实例端口区间隔离:
- 新增
DSHS_INSTANCE_PORT_BASE/DSHS_INSTANCE_PORT_SPAN,findFreePort()改为在区间内挑端口; - 各 worker 配互不重叠的区间(
w-47=42000+,w-106=43000+)⇒ 同一台 Manager 的127.0.0.1上永不撞号; - ✅ 副作用收益:不需要
portMap(不改"不同号")⇒ §4 S3 里"必须同时给ClusterHost加portMap"一并作废;src/worker/agent.ts的instanceHost也不用改。 - ⛔ 若将来确需回环别名:只能开
GatewayPorts clientspecified,且只能开在新建的 relay sshd 上 —— 开在主 sshd32022等于让持隧道密钥者可把端口绑到0.0.0.0,命中 R5(权限只准收窄)。
9.2 🔴 S3 的真实必要性上修为「在修一个真 bug」,不是"管道美化"
findFreePort()(src/supervisor/spawn.ts:53)只在 worker 自己那台机器 上随机取端口(调用点 src/supervisor/orchestrator.ts:595)⇒ 两台 worker 取到同号是常态;
而 tunnel.forward() 的返回值在 src/worker/agent.ts:303 与 :205 两处都被忽略 ⇒ 撞号时 -R 失败但静默;
Manager 随后仍按 127.0.0.1:<port> 拨 ⇒ 打到另一个用户的实例。
w-47 的本地实例与 w-106 的隧道落点共享 47 的 127.0.0.1 端口空间 ⇒ 两台机器即可触发。
9.3 🔴 S1 的部署面:本方案与 v1 交接单都漏了 47
S0 的 src/web/server.ts、src/supervisor/remote-spawner.ts、src/net/* 跑在 Manager(47);S2 的 hostsProvider 也在那儿。
实测:47 /opt/dshs/lib/ 至今无 net/,config.js 指纹 ≠ 本机 ⇒ 47 是 S0 之前的版本;S1 只上了 106(Worker) 侧。
⇒ 执行必须先做一步 P1:把 47 对齐到 640813e(零行为变化,风险最低),再上 S2。
9.4 ⚠️ S4 有方案缺口:中继一旦不在 Manager 主机上,"Manager 拨落点"就断
隧道落点在接收方那台机器的 127.0.0.1。中继换到别的机器 ⇒ 落点在那台机器上,Manager 拨不到。
⇒ 需要额外一跳:① Manager 侧向中继建出向隧道(ssh -L,把中继回环落点搬到 Manager 自己的回环),或 ② 中继侧把落点暴露到非回环地址 + nft 收窄(暴露面变大)。
倾向 ①(中继 sshd 的 gatewayports no 可保持不动 = 零权限扩大,且支持异地中继)。
⇒ 本轮 S4 只做同机 relay;⛔ 原 §4 S4 验收第 3 条"会合指向第二个中继实例"本轮不作判据(那要先定这一跳)。
9.5 ⚠️ S2 减负:不要加 address 列
endpoint 本身就是"要拨的地址",via 只补"经谁";再加 address = 同义双真相(回填后必然逐字相等,违反单一来源)。⇒ §4 S2 的 ADD COLUMN address text 作废,只加 via。
9.6 验收工具的现实约束(§4 未提,会直接卡住执行)
scripts/verify-cluster-cross.mjs:默认 MANAGER=http://127.0.0.1:13080(演练端口,现役是 3080)、AGENT_TOKEN=cross-machine-token(现役 dshs-worker-7f3a91c05e)、需 ADMIN_PW;
且它有生产副作用(幂等注册 w-106 并把容量改 4096 / 新建 crossuser* 用户 / 在 106 拉真实例 / 可选注册 w-106b 并迁移实例)⇒ 跑完必须清理;脚本头部注释"控制面 PG 在 106"已过时(PG 现在在 47)。
9.7 §8 三处勘误的复验结论
⚠️ 行号微差(本轮实测):§1.2 / §8 写的
proxy.ts:138-139实测应为136-137(host/port两行);admin.ts的注册路由实测在:181(POST /api/admin/hosts),不是:184。结论不变,只是行号偏了一两行。
- ①
proxy.ts:109✅ 再次确认仍是"发给上游 dsh 的Host头",⛔ 不可改;连接目标在proxy.ts:136-137(host/port取自endpointFor)。 - ② 两条
endpoint同形不同义 ✅ 确认(w-47=local/w-106=manager-ssh),别一刀切回填。 - ③ 47 cluster 配置在 drop-in ✅ 确认。