Files
dsh_ai1net_server/归档/交接单-20260924-归档/交接单_443兜底_20260917.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

41 KiB
Raw Blame History

交接单 · 443/TCP 兜底(覆盖网络线 · 序 ④)

线:覆盖网络线 | 序:④(定序 ③→④→⑤,见入口 §0) 性质:规划棒产出(本单只出单,⛔ 未动任何服务器、未改任何代码) 产出时间:2026-09-17 08:4x | 规划会话:覆盖网络线-序4规划棒-20260917-0837 唯一来源:覆盖网络_应用场景与待完善清单 §五(主线第 ④ 项)+ 覆盖网络_全球架构复盘 §3/§8 + 覆盖网络_补遗与参考方案 §2.4/§2.8/§三/§四 上一轮:序 ③ 已收官(交接单_一机一钥与信任根_20260917.md §8)⇒ ⛔ 不要重做 ②/③ 执行棒:按本单 §5 开工,不读规划会话上下文;收口必须回填 §8。


§1 目标

在 47 上为 relay 增加一条「不经 Cloudflare、不经门户站点 conf」的 443/TCP 直连入口,并把它作为第二条引导种子广播进签名目录,使只能出 443/TCP 的节点在 CF / 门户站点任一不可用时仍能完成会合与 relay 注册;全程 零新增公网端口、零新增证书、零新增域名、零花费。

判定"做完了没有" = §6 的 D1–D8 全绿(每条都能被第三方复现)。

为什么这是序 ④ 的最小正确形态(不是妥协,是已排过序):

  • 客户端只能出 443/TCP ⇒ 端口必须仍是 443,⛔ 不能用"非标端口"当兜底。
  • 现役唯一入口 = wss://alotbuy.com/dshs-relay,链路 = 客户端 → CF → 门户 nginx 443 块 → 127.0.0.1:20080 ⇒ CF 与门户站点 conf 是两个共用失败域。
  • 骨架里已存在多入口的承载物(DEFAULT_OVERLAY_SEED 常量位 / DSHS_OVERLAY_BOOTSTRAP_SEEDS 逗号多值 / 签名目录 relays[]+bootstrap[] / 逐 origin 失败 continue)⇒ 兜底不需要新协议、不需要新组件,只需要"补一条入口 + 广播 + 可解释"。
  • config.ts:167 的原文已写明该常量位的设计意图 = 「已持证书、不新增域名」⇒ 本单正是把它用起来。

§2 只读前置(⛔ 只读,不改)

执行前逐条核一遍,任何一条与"期望"不符 ⇒ 先停下来报告,别猜。 47 = 47.77.182.89、106 = 106.54.21.172(按既有惯例;ssh 别名以本机 ~/.ssh/config 为准)。

# 核什么 命令 期望
P1 三单元在跑 ssh 47 'systemctl is-active dshs dshs-relay dshs-pg nginx' 4×active
P2 监听面基线(收尾要逐字一致) ssh 47 'ss -lntp | grep -E ":(443|20080)\b"' nginx 0.0.0.0:443;node 127.0.0.1:20080(不是 0.0.0.0)
P3 现役入口在哪个 conf;目录端点的 location 是否已存在 ssh 47 'nginx -T 2>/dev/null | grep -nE "dshs-relay|dshs-overlay"' 命中门户 conf(alotbuy.com.conf);🔴 若 没有 /dshs-overlay/bootstrap 的 location ⇒ 本单 S1 要补(否则兜底入口拿不到签名目录)
P4 CF 现状 + 证书是否已覆盖兜底子域 dig +short alotbuy.com @1.1.1.1;ssh 47 'openssl s_client -connect 127.0.0.1:443 -servername relay-direct.alotbuy.com </dev/null 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"' 前者 = CF 段(104.x/172.6x);后者 SAN 含 *.alotbuy.com ⇒ 零新证;⛔ 若不含 ⇒ 停
P5 该机的引导链 env 现状(决定广播是否生效) ssh 47 'grep -rn DSHS_OVERLAY /etc/systemd/system/dshs.service.d/*.conf'|同样看 106 47 预期未配 DSHS_OVERLAY_BOOTSTRAP_SEEDS(= 只有内置常量)|106 若有 --url / DSHS_RELAY_URL ⇒ 压制引导链,须先记录(见 §5 S2-附)
P6 签名目录原文 curl -s --http1.1 https://alotbuy.com/dshs-overlay/bootstrap | head -c 400 200 + JSON,含 network / version / relays[] / bootstrap[] 与 sig
P7 relay 容量与身份现状 ssh 47 'curl -s 127.0.0.1:20080/status' 含 capacity、identityRequired、identityOk(序 ③ 后应 = 3)、online[]
P8 本机代码基线 git -C D:/github/dsh_shenxian rev-parse HEAD 640813e(⚠️ 工作区有未提交改动,⛔ 不要 checkout 掉)

判据先说清(P5 → S2 的因果):net/relay/main.ts 里 --url / DSHS_RELAY_URL 算"env 显式"、压制整条引导链(原文:压制引导链,运维最后手段);而 DSHS_OVERLAY_BOOTSTRAP_SEEDS 才是"入口列表"。⇒ 兜底要生效,入口必须走 seeds,⛔ 不能靠 DSHS_RELAY_URL。


§3 范围

3.1 要改的(且只有这些)

层 位置 改什么
47 · nginx 新文件(建议 /www/server/panel/vhost/nginx/relay-direct.conf) 新增一个 独立 443 server 块:server_name relay-direct.alotbuy.com(名字见 §4.2),证书路径从门户 443 块原样复制,两个 location:/dshs-relay → 127.0.0.1:20080|/dshs-overlay/bootstrap → 平台 web(127.0.0.1:3080,以 P3 实测为准)
47 · systemd /etc/systemd/system/dshs.service.d/*.conf DSHS_OVERLAY_BOOTSTRAP_SEEDS = 主入口在首位 + 兜底项(追加,⛔ 不改动其它行)
本机 · 代码 src/net/relay/client.ts(建连点 L415)、src/config.ts(配置字段)、test/overlay-*.test.mjs 新增"地址覆盖 + 指定 SNI"能力(详见 §5 S3),使兜底入口在 DNS 不可用/被污染时也能连

3.2 ⛔ 不动什么(防顺手扩大)

  • ⛔ 不动 alotbuy.com.conf 门户 443 server 块(server_name / 证书 / 既有 location 一律不碰)
  • ⛔ 不动 relay 进程:仍 127.0.0.1:20080、不新增 --host、不在 relay 侧做 TLS(relay 是纯 ws://,TLS 由 nginx 终结 —— 已核实)
  • ⛔ 不新增公网端口(客户端仍只走 443/TCP)、不新增 DNS 记录、不新增证书、不新增域名
  • ⛔ 不动 Cloudflare 配置;不动 dshs(Manager 主服务)与 dshs-pg
  • ⛔ 不碰序 ③ 的成果:四层密钥、identityRequired 语义、/etc/dshs/relay-keys.json
  • ⛔ 不碰数据面(打洞/UDP)、不碰 presence / 房间层(主线第 7 步)
  • ⛔ 不做跨机第二实例(L3 真容灾,见 §4.1-6)
  • ⛔ 不 commit、不 push

§4 决策点

4.1 已定项(规划棒已拍 —— 执行棒不得自行更改;要改必须回写 §8 并说明理由)

  1. 兜底通道 = 仍是 443/TCP 上的 wss,同协议同端口、零新增公网口。依据:复盘 §3「兜底通道 = HTTPS/443」+ 清单 §五 第 4 项。
  2. 兜底要分离的失败域按价值分三层,本单做 L1/L2,L3 明确后置:
    • L1 去 CF(最高价值):客户端支持"目标地址覆盖 + 指定 SNI"⇒ CF 不可用/被墙时仍可达。本单做。
    • L2 去门户站点 conf:47 上为 relay 建独立 server 块(独立 server_name)⇒ 门户 conf 被改坏/被面板重写时 relay 入口不受影响。本单做。
    • L3 去 nginx 进程与单实例:需换机或第二公网 IP ⇒ 入站面扩大(命中 R5)+ 属序 ⑤ 权限评估范畴 ⇒ ⛔ 本单不做,登记为序 ⑤/⑥ 候选。
  3. 入口列表的唯一载体 = DSHS_OVERLAY_BOOTSTRAP_SEEDS(逗号多值,已支持)。⛔ 不再用 --url / DSHS_RELAY_URL 表达入口 —— 它压制引导链,会把兜底关掉。
  4. 入口的在线轮换载体 = 签名目录:src/web/routes/overlay.ts 已把 relays[] = relayUrl + seeds、bootstrap[] = seeds 写进签名文档 ⇒ 改一次 47 的 seeds,全网已入网节点下一次刷新自动拿到兜底入口(不重装)。这是本单的主要收益机制,S2 必须验证它真的生效。
  5. 地址覆盖(IP)⛔ 不许塞进 URL 或签名目录 —— 必须走独立配置项。理由:directoryUrlFor() 会重写 path 并清掉 search/hash(覆盖信息会被吃掉);且 IP 进签名目录 = 把拓扑写进全网可见文档(违反"控制面下发全网名单"反模式 §四-4)。
  6. 不做容量承诺:45% 设计口径(异构纪律,⛔ 非同构 15%)只登记口径与待填参数位置(relay --max-hosts / /status.capacity / retryAfterMs),具体数值属序 ⑤ 参数表。
  7. 可解释是验收项,不是加分项:引导链已有 source= / detail= 与逐 origin 拒绝原因 ⇒ 必须能 grep 出"为什么走了兜底"。依据:补遗 §2.4-11 / §2.8-24。

4.2 交给执行棒自决(⛔ 不上升为提问)

兜底子域名具体字面(建议 relay-direct.alotbuy.com)· conf 放置方式(建议独立文件 —— 门户 conf 受宝塔面板管理,独立文件避免被面板重写)· 地址覆盖的配置项名与语法(建议 DSHS_OVERLAY_ADDR_OVERRIDES=<域名>=<IP> 逗号多值)· WebSocket 侧的注入方式(client.ts:415 的 WebSocketCtor 已是注入点,第二参 dispatcher 需同步放宽类型)· 种子项顺序与是否同时放 IP 直连项 · 缓存路径 · 备份文件命名 · 单测用例划分。

4.3 真需要用户拍板的(命中才问,且一轮只问这一句)

条件式:只有 S0 取证(或 S3 实测)证明「地址覆盖 + 指定 SNI」在当前 WebSocket 依赖下不可实现、且引入代码改动被否时,才需要你出手 —— 那时唯一缺口 = 兜底子域没有 DNS 记录,需要有人在 Cloudflare 侧加一条记录。两种做法:

A:你在 Cloudflare 加一条灰云 A 记录(relay-direct.alotbuy.com → 47.77.182.89)。  优点:零代码改动,客户端不需要新能力,工作量最小。  缺点:依赖你的账号与人工操作;DNS 层被污染时仍不可达(只解 CF,不解 DNS 层)。

B:把 CF 的只读/受限 API 凭据给我,之后这类记录我自己维护。  优点:后续同类入口(第五步参数表、第六步最小形态)不再需要人工介入。  缺点:凭据外发是一次权限扩大,需要你评估;且我仍要处理"DNS 层不可达"的残余风险。

我的倾向:A 优先(一次性、可随时撤回)。若 S3 的地址覆盖实测通过,则 A/B 都不需要,本单零依赖关闭。

4.4 技术实现裁决顺序

按 dsh-decision-method §4.4;规则冲突按下序取首个命中项:R8(开发环境服务器 ⇒ 该动就动,动手前一句话说明) → §1 边界内自决清单(部署/重启/改配置/nginx/技术选型) → §3 其余红线(R5/R7/R9/R10 永远是硬约束)。⛔ 冲突 ≠ 门禁。


§5 步骤(S0–S5,每步自带一次可执行的验证)

S0 · 只读取证(⛔ 不改任何东西)

跑完 §2 的 P1–P8,产出一段"现状快照"(命令 + 原文输出 + 判定)。任何一条与期望不符 ⇒ 停下报告。 额外取三件(本单设计的必要输入):

  • 门户 443 块的完整原文(证书路径 / http2 on / map $http_upgrade 位置)⇒ S1 要原样复制;
  • /dshs-overlay/bootstrap 当前由哪个后端提供(3080 还是别的)⇒ S1 的 proxy_pass 目标;
  • 47 / 106 上 DSHS_RELAY_URL、DSHS_RENDEZVOUS_URL、DSHS_OVERLAY_* 的全部现值。

S1 · 兜底入口落 nginx(47)

新建独立 server 块(拷贝门户 /dshs-relay 的 location 正文,只改 server_name 与证书两个变量): listen 443 ssl; http2 on; + server_name <兜底子域>; + location /dshs-relay { proxy_pass http://127.0.0.1:20080; … } + location /dshs-overlay/bootstrap { proxy_pass http://<P3 实测后端>; … }。 nginx -t 必须先过,再 reload(先备份:cp <conf> <conf>.bak-20260917-HHMM-pre-443fb)。

验证(47 本机):curl -i -s -N --http1.1 --resolve <兜底子域>:443:127.0.0.1 -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" https://<兜底子域>/dshs-relay | head -3 ⇒ HTTP/1.1 101 Switching Protocols;目录端点 ⇒ 200 + 含 "sig"。

S2 · 种子广播(47)

DSHS_OVERLAY_BOOTSTRAP_SEEDS=https://alotbuy.com/dshs-relay,https://<兜底子域>/dshs-relay(主入口必须在首位)→ daemon-reload → restart dshs。

S2-附(若 P5 发现 106 上有压制项):先把 106 的 --url / DSHS_RELAY_URL 撤掉(改为 seeds 表达),否则 106 永远走 env、拿不到兜底 —— ⚠️ 这一步会改变 106 的取址路径,属于"会中断在线用户"的动作 ⇒ 动手前一句话说明,并准备好回滚(§7)。

验证:curl -s --http1.1 https://alotbuy.com/dshs-overlay/bootstrap | python -c "import sys,json;d=json.load(sys.stdin);print(d['relays'],d['bootstrap'])" ⇒ 两个数组都含兜底项,且主入口在首位;version 递增。

S3 · 客户端「地址覆盖 + 指定 SNI」(本机代码)

在 client.ts:415 的建连点接入"覆盖地址 + SNI = 域名"(WebSocketCtor 已是注入点)。加单测:覆盖生效时实际连的是覆盖 IP、TLS 校验用的是域名;未配覆盖时行为与今天逐字一致(不退化)。 npm test 必须 Node 22。scp 到 47(⚠️ 47 的 lib 路径 = /opt/dshs/lib/)。

验证:本机把兜底子域指向一个不存在 DNS 的名字 + 覆盖项指向 47 真实 IP ⇒ 仍 HTTP/1.1 101。

S4 · 降级可解释 + 降级实测(⛔ 不许弄坏生产入口)

只在测试进程上验降级:用 DSHS_OVERLAY_BOOTSTRAP_SEEDS=https://127.0.0.1:9/dshs-relay,https://<兜底子域>/dshs-relay 起一个独立 relay-client 进程(主入口故意不可达)⇒ 日志必须出现"逐 origin 拒绝原因 + 最终采用兜底项"两行,且注册成功。⛔ 不允许为验收而改生产的 seeds 顺序。

验证:journalctl/stdout 里可 grep 到 source=、detail=、兜底项 URL 与成功注册行。

S5 · 端到端复验 + 不退化检查(47/106 双机)

① w-47(19100) 与 w-106(19000) 实例面仍通;relay /status 的 online[] 含两台;② identityRequired/identityOk 与 S0 逐字一致;③ 监听面与 P2 逐字一致(零新增公网口);④ 门户 curl -H "Host: alotbuy.com" 127.0.0.1:3080/ = 200;⑤ relay 侧 not_found=0;⑥ npm test 全绿。 ⑦ 45% 口径登记:把"兜底启用后,relay 容量须按 45% 节点走中继核算"写进回报,并留给序 ⑤ 参数表(⛔ 本单不填数值)。


§6 验收(判据清单)

# 判据 命令 期望
D1 兜底入口 101 curl -i -s -N --http1.1 --resolve <兜底子域>:443:47.77.182.89 … /dshs-relay | head -1 HTTP/1.1 101 Switching Protocols(⚠️ 必须 --http1.1,http2 on 会假 404)
D2 兜底入口不经 CF 同上但目标 IP = 47 真实 IP;且 dig +short <兜底子域> 无记录 / 与 CF 段不同 ⇒ 未走 CF
D3 兜底入口不依赖门户 conf ssh 47 'nginx -T | grep -c "server_name <兜底子域>"';临时把门户块里 location /dshs-relay 注释 → 兜底仍 101 → 恢复 命中 1;门户块失效时兜底不受影响(⚠️ 事后必须逐字还原)
D4 目录端点同源可用 curl -s --http1.1 --resolve <兜底子域>:443:47.77.182.89 https://<兜底子域>/dshs-overlay/bootstrap 200 + JSON 含 sig(目录与中继同源是既有约定,缺了则兜底拿不到目录)
D5 广播生效 见 S2 命令 relays[] 与 bootstrap[] 均含兜底项,主入口首位,version 递增
D6 降级可解释 见 S4 日志含逐 origin 拒绝原因 + 采用兜底项 + 注册成功
D7 零退化 P2 复跑 + 双实例面 + identityOk + 门户 200 + npm test 与 S0 快照逐字一致(除"多了一个 server 块/一条 seeds")
D8 零新增暴露面 ssh 47 'ss -lntp | wc -l'、nft list ruleset、公网 curl 47.77.182.89:<非443> 监听口数量与 S0 一致;无新增公网端口

判定:D1–D8 全绿 = 本单完工。任一红 ⇒ 按 §7 该项回滚,并在 §8 写清"卡在哪 + 已做到哪一步 + 什么条件一出现必须回头解决"。


§7 回滚

项 回滚
S1 nginx cp <conf>.bak-20260917-HHMM-pre-443fb <conf> → nginx -t → nginx -s reload(或删掉新建的独立 conf 文件后同两步)
S2 seeds 从 drop-in 撤掉兜底项 → daemon-reload → restart dshs(⚠️ 主入口项必须保留,别整行删)
S2-附 106 取址 恢复 106 原 DSHS_RENDEZVOUS_URL / --url 写法 → daemon-reload → restart 该单元
S3 代码 本机回滚靠备份副本(⛔ 未授权 commit,git checkout -- 会连带丢掉工作区其它未提交改动);47 侧回滚 = 恢复 /opt/dshs/lib/ 旧的 net/relay/*.js 副本(铺前必须先备份)
S4 临时进程必须回收(pidfile 或 systemctl;⛔ 不要 pkill -f,会命中 ssh 自身命令行)

§8 回报格式(执行棒按此格式收口)

## §8 执行回报(执行棒 · 2026-09-17 HH:MM)
1. S0 快照:P1–P8 逐条 = 命令原文 + 输出 + 判定(不符的先说)
2. 落盘清单:新建/改动的**每个文件绝对路径** + 备份文件名 + 改前改后指纹(md5/hash-object)
3. D1–D8:逐条命令原文 + 原文输出 + 绿/红
4. 降级实测(S4):日志原文两行(拒绝原因 + 采用兜底)
5. 不退化:P2 复跑对照 / identityOk / 门户 200 / npm test 计数(a/b/c)
6. 45% 口径:登记行原文(⛔ 无具体数值)
7. 未过项 / 遗留:逐条写"卡在哪(证据)+ 已做到哪一步 + 什么条件一出现必须回头解决"
8. 指纹:本单回填后 `md5sum`

附 A · 本规划棒已核实的事实(执行棒不必重复探索)

事实 依据(本地代码 / 文档)
relay 没有 TLS,opts.host ?? '127.0.0.1'、http.listen(port, host) ⇒ 只绑回环,TLS 全由 nginx 终结 src/net/relay/server.ts:359/405/410(日志原文 loopback only)
relay 没有 --host 参数(--host 在 client 模式 = hostId)⇒ "让 relay 直听 443" 必须改代码 ⇒ 已判为更差候选,拍掉 src/net/relay/main.ts:55–92
引导链顺序 = ① env 显式(压制)→ ② 缓存目录(未过期)→ ③ 取目录(缓存 bootstrap[] 优先 + seeds),逐 origin 失败 continue src/net/relay/main.ts:216–232、src/net/relay/directory.ts:530+
--url / DSHS_RELAY_URL = "env 显式",压制引导链;入口列表变量 = DSHS_OVERLAY_BOOTSTRAP_SEEDS(逗号多值) 同上 + directory.ts:74–79
内置种子常量 = https://alotbuy.com/dshs-relay(单值);常量位注释已写"已持证书、不新增域名" directory.ts:72、config.ts:167–178
目录端点路径 = /dshs-overlay/bootstrap;"引导地址 = 中继入口同源" directory.ts:45、config.ts:178
签名目录自动带上入口:relays[] = relayUrl + seeds、bootstrap[] = seeds ⇒ 改 seeds 即全网广播 src/web/routes/overlay.ts:58–60
种子条目语法 = 必须是合法绝对 URL(http/https/ws/wss);directoryUrlFor() 会重写 path 并清 search/hash;isPublicHost() 拒绝私网/CGNAT/IPv6;上限 MAX_ENTRIES=8、MAX_ENTRY_LEN=512 directory.ts:190–208 / 322–337 / 355–372 / 61–62
客户端建连 = new Ctor(this.opts.url),WebSocketCtor 可注入(默认全局 WebSocket)⇒ 覆盖地址/SNI 有落点 src/net/relay/client.ts:63/415
目录刷新周期 = 300 s;取目录超时 = 5 s directory.ts:58/472
relay 容量/准入 = --max-hosts(现为 0 = 不限)+ /status.capacity + at-capacity/retryAfterMs(5000 ms) main.ts argv、server.ts:63
47 的 relay 单元 = /etc/systemd/system/dshs-relay.service(--port 20080 --keys-file /etc/dshs/relay-keys.json);拨号白名单 drop-in = dshs-relay.service.d/dialers.conf 交接单_relay落地R2-R4_20260916.md §9/§11
门户 443 块 = /www/server/panel/vhost/nginx/alotbuy.com.conf;dsh.alotbuy.com.conf 是遗留 301 域名(⛔ 不能往它里面加 location) 同上 §9

附 B · 硬约束复述(防走偏)

  • ⛔ 本轮只出单:⛔ 不改服务器、不改代码、不 commit、不 push、不做序 ⑤、不碰 presence。
  • ✅ 只做正向迭代(R11):本单任何一步若让任一维度净变差 ⇒ 立即停下复盘;拿不出正向做法 ⇒ 停止执行。
  • ✅ 权限只准收窄(R5):本单零新增公网端口、零新增入站面、零凭据外发;若有任一改动会扩大权限面 ⇒ 先停下报告。
  • ⛔ R7:只做 §3.1 列出的改动;执行中发现别的"小问题"先报告、后动手。
  • ⚠️ curl 判据必须 --http1.1(门户 http2 on,否则假 HTTP/1.1 404/HTTP/2 404)。
  • ⚠️ 47 的 lib = /opt/dshs/lib/、106 = /opt/dshs-cluster/lib/;部署 = 本机 build 后 scp;npm test 必须 Node 22。
  • 📌 本单不做容量承诺(参数表 = 序 ⑤);不做跨机真容灾(L3,需 R5 权限评估)。

§8 执行回报(执行棒 · 2026-09-17 09:1x)

执行会话:覆盖网络线-序4执行棒 | 锁:--claim-exec 抢到(09:00)→ 收口 --release-exec 本单 md5(回填前)= b9aa6bbc0a458481627f7aeb1f17ab54 —— 与规划棒交付值逐字一致 ⇒ 无人改动过。

8.1 S0 快照(P1–P8:命令原文 + 输出 + 判定)

先把三处"与期望不符"说清(都是判据本身要改口径,不是故障):

(1)P1 的 nginx 一栏 = inactive。 真因:本机 nginx 不归 systemd 管 —— 主进程 = /www/server/nginx/sbin/nginx -c /www/server/nginx/conf/nginx.conf(宝塔直接拉起),nginx.service 是 sysv 生成的壳、状态恒 inactive (dead)。 ⇒ 判据改为 ss -lntp | grep :443 + pgrep nginx。服务实际正常(443 在听、门户 200)。

(2)P3 的目录端点在 nginx -T 里没有独立 location。 实测只命中门户块的 location /dshs-relay;/dshs-overlay/bootstrap 是被门户块 location /(→ 127.0.0.1:3080)顺带承载的。 ⇒ 按 P3 的"🔴 若没有 ⇒ S1 要补",S1 补了独立 location;并因此发现第二件事(见 8.4-D4)。

(3)P4 的 DNS:relay-direct.alotbuy.com 能解析,且落在 CF 段。 判据:随机名 zzz-rand-9f3a.alotbuy.com 同样解析到同一对 CF IP ⇒ 本域是 *.alotbuy.com 泛解析(橙云代理),不存在"没有记录"这一情形。 ⇒ 这条使 D2 的字面判据不可满足(实质判据已证,见 8.4-D2)。

# 命令(原文) 输出(原文摘要) 判定
P1 systemctl is-active dshs dshs-relay dshs-pg nginx active active active inactive ⚠️ 见上(nginx 改口径)
P2 ss -lntp | grep -E ":(443|20080)\b" 127.0.0.1:20080(node) + 0.0.0.0:443(nginx) ✅ relay 只绑回环(不是 0.0.0.0)
P3 nginx -T | grep -nE "dshs-relay|dshs-overlay" 仅 location /dshs-relay { ⚠️ 见上(目录端点由 location / 承载)
P4 openssl s_client … -servername relay-direct.alotbuy.com | grep -A1 "Subject Alternative Name" DNS:*.alotbuy.com, DNS:alotbuy.com ✅ 零新证;DNS ⚠️ 见上
P5 grep -rn "DSHS_OVERLAY|DSHS_RELAY_URL|DSHS_RENDEZVOUS" /etc/systemd/system/dshs.service.d/ /etc/dshs.env | 106 同样 47:无 DSHS_RELAY_URL / 无 seeds(cluster.conf:15 注明 P0-2 已清)|106:无 --url / DSHS_RELAY_URL,但有 DSHS_RENDEZVOUS_URL=wss://alotbuy.com/dshs-relay ✅ 符合预期 ⇒ S2-附 不触发(见 8.2)
P6 curl -s --http1.1 --resolve alotbuy.com:443:127.0.0.1 https://alotbuy.com/dshs-overlay/bootstrap 200 + {"version":1,…,"relays":["https://alotbuy.com/dshs-relay"],"bootstrap":["https://alotbuy.com/dshs-relay"],"sig":"…"} ✅
P7 curl -s 127.0.0.1:20080/status online=[manager, w-106]、capacity{max:0,used:2}、identityRequired:true、identityOk:3、trustedSigners:1、revokedHosts:0 ✅
P8 git -C D:/github/dsh_shenxian rev-parse HEAD 640813e84e5e48c56ca39d765d7b195884e58ebc ✅
D8基 ss -lntp | wc -l / nft list ruleset | wc -l 79(表头 1 + 78 条)/ 72 ✅ 基线

⚠️ S0 顺带核出、必须记下来的一条事实:src/net/relay/ 整目录未被 git 跟踪 —— git status --porcelain -- src/net/relay/ = ?? src/net/relay/;git ls-tree -r --name-only HEAD -- src/net/relay/ = 0 个文件。 ⇒ 覆盖网络线(R1 起)的代码只存在于工作区 + 部署产物里。这既是"⛔ 别 git checkout"那条纪律的实证依据,也意味着 47 上的 *.bak-*pre-443fb 是本单唯一的改前基线(见 8.3)。

8.2 S2-附:不执行(前提不成立)+ 一处必须报告的风险(未动手)

P5 实测 106 没有 --url / DSHS_RELAY_URL ⇒ S2-附 的触发条件不满足,未执行(符合单里"若…才有"的条件式写法)。

但顺手核出的这条必须报(⛔ 未动手):106 上确有 DSHS_RENDEZVOUS_URL=wss://alotbuy.com/dshs-relay,它不是 DSHS_RELAY_URL、不参与引导链,而是 worker agent 的会合地址:

  • 依据(本地代码,逐行可核):src/worker/agent.ts → rendezvousRaw = options.tunnelTarget ?? config.clusterRendezvousUrl ?? '';src/config.ts → clusterRendezvousUrl = DSHS_RENDEZVOUS_URL ?? DSHS_TUNNEL_TARGET ?? '';随后 rendezvousRaw === '' ? undefined : … 直接构造 RelayTunnel({ url: rendezvousRaw })。
  • ⇒ 106 的 agent 面根本不消费引导链:该变量一旦按 S2-附 的字面撤掉,rendezvousRaw 变空 ⇒ tunnel === undefined ⇒ 106 的 agent 隧道直接不起(生产回归)。
  • ⇒ 结论:S2-附 的写法("撤掉 env、改由 seeds 表达")在 106 上不成立,未执行,106 现状逐字未动(登记为遗留 8.7-②)。

8.3 落盘清单(绝对路径 + 备份 + 改前/改后指纹)

本机代码(D:\github\dsh_shenxian) —— ⚠️ 全部未 commit、未 push(本单明令)

文件 动作 指纹 git hash-object
src/net/relay/addr-override.ts 新建(地址覆盖模块) 4703a73ac82b66d113005c7fc10557d64b76407c
src/net/relay/client.ts 改(建连点注入) 5152378446987a97fa3dbff64398c6e2737d03f5
src/net/relay/directory.ts 改(取目录腿注入 + 同源优先) 36e2a86874279c818faa20aa2b66687f92cf2f25
test/overlay-bootstrap.test.mjs 改(+7 用例 L1-A…G) 8aef224e5053c78fe56de716d6e408b549475a7e

本机取不到 git 改前基线:src/net/relay/ 与 test/overlay-bootstrap.test.mjs 均不在 HEAD(见 8.1 注)⇒ 改前基线以 47 上的 .bak-* 为准(下表)。

47 · /opt/dshs/lib/net/relay/(部署 = 本机 npm run build 后 scp)

文件 动作 改前(备份) 改后(生效)
addr-override.js 新增 —(新文件) a2bb27c3b021b49c1e35b707221a9871
client.js 覆盖 client.js.bak-20260917-0901-pre-443fb = 8cbeeb029e856ce174dd91fdd44a739a bc24cb70d047f0e711a17db87dfa6246
directory.js 覆盖 directory.js.bak-20260917-0901-pre-443fb = a5ca2103dee32dde154d965da9d471e0 214905d476c185baf87358c8a5e9d25e

47 · 配置

文件 动作 指纹
/www/server/panel/vhost/nginx/relay-direct.conf 新建(独立 443 server 块) b7851b93c7bf885d73fc250813a4b42f
/etc/systemd/system/dshs.service.d/overlay-443fb.conf 新建(seeds + 地址覆盖) 8eac06167cc4b1a772b64b640f150a89
/www/server/panel/vhost/nginx/alotbuy.com.conf 未改(D3 临时改后用备份逐字节还原) 改前 = 改后 = a747cabc2c03911e831a714f1e89a47d ✅
…/alotbuy.com.conf.bak-20260917-0900-pre-443fb 备份(符合 §5 命名要求) a747cabc2c03911e831a714f1e89a47d

106:⛔ 未动任何文件(S2-附 不触发,见 8.2)。

8.4 D1–D8 逐条(命令原文 + 原文输出 + 绿/红)

# 命令(原文摘要) 原文输出 判定
D1 curl -i -s -N --http1.1 --resolve relay-direct.alotbuy.com:443:<IP> -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" https://relay-direct.alotbuy.com/dshs-relay | head -1 HTTP/1.1 101 Switching Protocols 🟢
D2 ① 同 D1 但 --resolve …:443:47.77.182.89(本机从公网探)② DNS 探针 ① HTTP/1.1 101、Server: nginx、无 cf-ray / Server: cloudflare ② relay-direct.alotbuy.com -> ['104.21.44.42','172.67.194.206'](CF 段) 🟢 实质 / 🔴 字面
D3 nginx -T | grep -c "server_name relay-direct.alotbuy.com";临时注释门户块 location /dshs-relay → 复测 → 还原 1;门户 = HTTP/1.1 502 Bad Gateway(已失效)、兜底 = HTTP/1.1 101(不受影响);还原后 md5 = a747cabc…(逐字节一致)、门户 101 / 兜底 101 / 首页 200 🟢
D4 curl -s --http1.1 --resolve relay-direct.alotbuy.com:443:<IP> https://relay-direct.alotbuy.com/dshs-overlay/bootstrap HTTP=200 + {"version":1,…,"relays":["https://alotbuy.com/dshs-relay","https://relay-direct.alotbuy.com/dshs-relay"],"bootstrap":[…],"sig":"…"} 🟢
D5 curl -s --http1.1 https://alotbuy.com/dshs-overlay/bootstrap relays = ["https://alotbuy.com/dshs-relay","https://relay-direct.alotbuy.com/dshs-relay"](主入口首位);bootstrap 同;issuedAt 由 …00:54:03 → …01:01:46 🟢("version"口径见下)
D6 见 8.5 见 8.5(拒绝原因 + 采用兜底 + 注册成功) 🟢
D7 P2 复跑 / 双机面 / identity / 门户 / npm test 见 8.6(逐项一致) 🟢
D8 ss -lntp | wc -l / nft list ruleset | wc -l / 外网探 5 个端口 79(= S0)/ 72(= S0)/ 全部"关闭" 🟢

D2 的绿/红必须说清:字面判据要"dig 无记录 / 与 CF 段不同",而本域是 CF 泛解析 ⇒ 字面不可满足(🔴)。实质判据("兜底入口不经 CF")已用两条独立证据坐实 🟢: ① --resolve 直连 47 真实 IP ⇒ 101,响应头 Server: nginx、无任何 CF 特征头; ② 客户端侧(8.5 的 A 轮)把该域覆盖到 127.0.0.1 ⇒ 仍 101 且注册成功 ⇒ 链路里没有 CF。

额外数据点:走真实 DNS(= CF) 的同一请求也 101 ⇒ 兜底入口在 CF 路径下同样可用(并非"只在直连时可用")。

D5 的"version 递增"必须说清:目录的 version 是结构版本(directory.ts 的 DIRECTORY_VERSION = 1,常量)⇒ 永不变,"递增"这条判据物理上不成立。判"确有更新"应看 issuedAt 前移 + 两个数组内容变化,二者本次均已取到(见上表)。

D4 顺带发现并已修的一处(S1 施工时暴露):平台(3080)先按 Host 做租户路由、再进路由表,直接用兜底子域回源会命中 404 {"error":"unknown_user"}。 修法 = 该 location 回源时把 Host 改写为既有目录 origin(alotbuy.com)—— 即"同源"在入口层的等价翻译,发的就是门户今天在发的同一个公开只读请求,不扩大任何权限(该 location 用 = 精确匹配、本块其余路径一律 404)。

8.5 S4 降级实测(日志原文)

只在独立测试进程上做:/tmp/fb443/lib = /opt/dshs/lib 的副本,⛔ 未改生产 seeds;DSHS_OVERLAY_BOOTSTRAP_SEEDS=https://127.0.0.1:9/dshs-relay,https://relay-direct.alotbuy.com/dshs-relay。

A 轮(DSHS_OVERLAY_ADDR_OVERRIDES=relay-direct.alotbuy.com=127.0.0.1)—— 成功,日志原文:

[addr-override] 覆盖生效:relay-direct.alotbuy.com -> 127.0.0.1(IPv4,直连该 IP,TLS SNI 仍为 relay-direct.alotbuy.com)
[overlay-dir] ⚠ 拒绝 https://127.0.0.1:9/dshs-overlay/bootstrap(unreachable: fetch failed)—— 未签名 / 签名不符的目录**不写缓存、不采用**
[overlay-dir] ↪ 同源优先:目录由 https://relay-direct.alotbuy.com/dshs-overlay/bootstrap 答出 ⇒ 采用其同源中继入口 wss://relay-direct.alotbuy.com/dshs-relay(不在 relays[] 首位;首位 wss://alotbuy.com/dshs-relay 本次未被采用)
[relay-client] registered host=w-47 network=ops session=7cbddf7256503e02 accepted=[19999] clockSkew=4ms

① 逐 origin 拒绝原因 ✅ ② 采用兜底项 ✅ ③ 注册成功 ✅ ④ 同时坐实 S3 的「地址覆盖 + 指定 SNI」:TCP 到 127.0.0.1、SNI = relay-direct.alotbuy.com、证书(*.alotbuy.com)校验通过。 (首轮曾用 --ports 29999,被 relay 以 AUTH DENY … port-out-of-range retryable=false 拒 —— 属测试参数越界,改 19999 即通过。)

B 轮(<IP>=192.0.2.1,TEST-NET 不可路由)= 判定对照:

[overlay-dir] ⚠ 拒绝 https://relay-direct.alotbuy.com/dshs-overlay/bootstrap(unreachable: The operation was aborted due to timeout)
[overlay-dir] ⚠ 取目录全部失败 ⇒ 回落到内置种子地址本身:wss://127.0.0.1:9/dshs-relay

⇒ 覆盖确实改写了目标地址(若没生效,DNS 会给 CF、而 CF 是通的 ⇒ 不会超时)。这是 A 轮结论的反证。

回收:测试进程用完即走(timeout 收尾),业务面已核无残留(见 8.6 末条)。

8.6 不退化(P2 复跑对照 / identity / 门户 / npm test)

项 S0 收口 判定
监听面 127.0.0.1:20080 + 0.0.0.0:443 逐字一致 ✅
监听口总数 79 79 ✅
nft 行数 72 72 ✅
online[] [manager, w-106] [manager, w-106] ✅
endpoints w-106:19000 / 21000 online 逐字一致(S4 残留的 w-47:19999 已清,见下) ✅
identityRequired / trustedSigners / revokedHosts true / 1 / 0 true / 1 / 0 ✅
47 agent(19100) / 实例面(3080) 200 / 200 200 / 200 ✅
106 agent(19000) / dshs-worker — 200 / active ✅
门户首页 200 200 ✅
npm test(Node 22.22.2) — 137 用例 / 136 通过 / 1 跳过 / 0 失败(含本单新增 7 条 序④·L1-A…L1-G) ✅
兜底日志里的 404 — 仅 /portal.html、/ 两条 = 我自己收窄验证的探针,无意外 404 ✅

计数器口径澄清(否则会被读成回归):authed / authFailed / identityOk 是累计计数,"逐字一致"物理上不成立。本次全部增量都能逐条归因:

  • authFailed=3(中间态)= 2 × [relay] AUTH TIMEOUT(= D1/D2 的 curl 101 探针:连上但不发 HELLO,relay 等鉴权超时)+ 1 × [relay] AUTH DENY … port-out-of-range(= A 轮那次的参数越界)。
  • identityOk 增量 = dshs / dshs-relay 每次重启后 manager 重连各 +1。
  • 收尾重启了一次 dshs-relay 清账(顺手清掉 S4 残留的 w-47:19999 端点与其动态回环口 43439 —— 它一度把监听口顶到 80)⇒ 收口态:online=[manager,w-106]、endpoints 只余 w-106 两条、监听口回 79、identityOk=2(两台节点各一次认证成功)、authFailed=1(收口后又打了一次 101 探针)。 ⇒ 读数提醒:101 探针本身会污染 authFailed。以后看这个计数器,先扣掉自己打的探针次数。

8.7 未过项 / 遗留(逐条:卡在哪 + 已做到哪一步 + 什么条件一出现必须回头解决)

① D2 字面判据不可满足(实质已证)

  • 卡在哪:*.alotbuy.com 是 CF 泛解析(证据:随机名 zzz-rand-9f3a.alotbuy.com 同样解析到 104.21.44.42 / 172.67.194.206)⇒"该子域没有 DNS 记录"这个前提不存在。
  • 已做到哪一步:实质判据(不经 CF)已用两条独立证据坐实(8.4-D2);且 §4.3 的 A / B 两个候选都不需要(既不需要人工加记录,也不需要 CF 凭据)⇒ 本单零依赖关闭。
  • 什么条件一出现必须回头解决:若要让解析层也不经 CF(例如应对 DNS 层投毒/污染),才需要把该子域设成灰云直连 A 记录;在那之前,运行期的"去 CF"由 DSHS_OVERLAY_ADDR_OVERRIDES 保证。

② 106 的 agent 面不吃引导链(已定位,⛔ 未动手)

  • 卡在哪:见 8.2 —— worker/agent.ts 把 DSHS_RENDEZVOUS_URL 直接当 relay URL 使用,不经过 resolveOverlayRelay。
  • 已做到哪一步:定位到行、判定"S2-附 字面执行会造成生产回归(tunnel === undefined)"⇒ 未执行;106 现状逐字未动。
  • 什么条件一出现必须回头解决:要做"106 agent 面也能用兜底",必须先让 worker 侧接入引导链(改 agent.ts / relay-tunnel.ts)—— 属序 ⑤范围,⛔ 不在本单。

③「兜底项在取址路径上原本永远选不中」—— 本单已补最小修法

  • 卡在哪(原缺陷):relays[] 由 seeds 按序生成、pickFromDoc 取首位(= 主入口),而 S2/D5 又要求主入口首位 ⇒ 生产链路上没有任何一支会去连兜底项,§1 的目标与 §5-S4/D6 的前提同时落空(S4 之所以"看起来"能过,只因为测试里首个 origin 用了回环口 127.0.0.1:9、被 publicRelayEntries 过滤掉 —— 属测试参数的偶然,不是生产机制)。
  • 已做到哪一步:补了**「同源优先」(sameOriginRelayUrl):谁答出目录,就用谁的同源中继入口。它不是新概念** —— 正是既有约定「引导地址 = 中继入口同源」在选择时刻的落地。S4/A 与新增用例 L1-F / L1-G 分别证明"兜底被采用"与"主入口通时逐字不变"。
  • 为什么必须改(按 §4.1"要改必须回写 §8 并说明理由"):① 不动任何 §4.1 已定项 —— seeds 顺序仍是主入口首位,目录内容与 D5 判据不变;② 零退化 —— 主 origin 通时同源项就是首位,行为逐字一致(L1-G 断言);③ 不补它 ⇒ 本单只交付一个"能 101 但没有任何客户端会去连"的端点,§1 的目标不成立(只剩装饰性)。
  • 什么条件一出现必须回头解决:若将来 relays[] 引入"非首位更优"的显式优先级语义(序 ⑤ 参数表可能引入),需重新定义"同源优先"与它的先后关系。

④ 引导链的缓存两支没有"答出者"信息(已知边界)

  • 卡在哪:第 ②/④ 步(新鲜/过期缓存)只存 doc+sig,不记"当初哪个 origin 答的"⇒ 这两支仍走 relays[] 首位。
  • 已做到哪一步:同源优先只作用于第 ③ 步(取目录成功),已在代码注释里写明。
  • 什么条件一出现必须回头解决:CF 打挂后,已入网节点要等一个刷新周期(refreshAfterSeconds,现为 300 s)才切到兜底。若要"立刻切",需在缓存里记 origin。

⑤ 顺带发现、⛔ 未动手的两项(R7:先报告、后动手)

  • bt-server 这个 ssh 别名端口已陈旧:~/.ssh/config 写 Port 32022,实测 Connection refused;-p 22 正常。⇒ 本次全程用 -p 22,未改 ~/.ssh/config。
  • src/net/relay/ 整目录未被 git 跟踪(HEAD 里 0 文件)⇒ 覆盖网络线代码只在工作区 + 部署产物里。未做任何 git 动作(本单明令不 commit / push)。

8.8 45% 口径(登记,⛔ 无具体数值)

登记行:兜底启用后,relay 容量须按 45% 的节点走中继核算(异构纪律,⛔ 不是同构的 15%)。 待填参数位置(留给序 ⑤ 参数表,本单不填数值):relay 侧 --max-hosts(现为 0 = 不限);/status 的 capacity{max,used};满载退避 at-capacity / retryAfterMs(现 5000 ms)。

8.9 指纹

  • 回填前(规划棒交付)= b9aa6bbc0a458481627f7aeb1f17ab54(执行棒开工前已复核,逐字一致)
  • 回填 §8 正文后(尚未写本节时)= 594e0b404bef18fb3959e48901153649(40996 字节)
  • 本节口径(推荐核对用,可复现):整个 §8.9 不计入 ⇒ 40997 字节。复核命令: sed '/^## 8\.9 指纹$/,$d' 交接单_443兜底_20260917.md | md5sum ⇒ 3d15b94069f289124d92da367e806bb5 (比上一行多 1 字节 = 正文与本节之间的空行)
  • 全文件 md5:请用 md5sum 交接单_443兜底_20260917.md 现取 —— ⛔ 此处故意不内嵌数值: 它包含本节自身,写进去即刻失效(自指)。