413 lines
41 KiB
Markdown
413 lines
41 KiB
Markdown
# 交接单 · 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` 现取 —— ⛔ 此处**故意不内嵌数值**:
|
|||
|
|
它包含本节自身,写进去即刻失效(自指)。
|