849 lines
93 KiB
Markdown
849 lines
93 KiB
Markdown
# 交接单 · 观测口径重构(OBS-11 计数 → 白名单集合)+ 在册小缺陷清算排序(2026-09-17)
|
||||
|
|
|
|||
|
|
- **状态**:⏳ 待执行(**规划棒产物**,2026-09-17 14:5x 出单)
|
|||
|
|
- **出单者**:覆盖网络线-序11规划棒(automation `3d4dffc0-356e-4bc6-9628-b7d664da7db9`)
|
|||
|
|
- **执行方式**:规划与执行分离 ⇒ 执行会话**只按本单开工、不读规划会话上下文**
|
|||
|
|
- **上游依据**(现取证据,⛔ 不必重跑):
|
|||
|
|
1. `交接单_检测时延与deadline_20260917.md` **§8.8-4 ③ 逐口对账**(序⑩ 收口回填)—— OBS-11 `78→77` 的名字 = **`20000`**(47 实例档、无活跃实例)
|
|||
|
|
2. 同单 **§8.8-4 末段「🔴 ③ 的副产品」** —— 计数判据的两处结构缺陷(ⓐ 对状态敏感 ⇒ 假红|ⓑ 对替换式变化不敏感 ⇒ 假绿)
|
|||
|
|
3. `参数表_覆盖网络_20260917.md` §6(`OBS-11` 现判据 = `LISTEN_COUNT` / `NFT_RULES` **相等**)
|
|||
|
|
- **成本纪律**:本单的取证命令**都已写死在 §2 / §6**,执行会话**照抄即可**(⛔ 不要另做全库探索)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §1 目标
|
|||
|
|
|
|||
|
|
**一句话(可判定「做完了没有」)**:`scripts/overlay-probe.cjs` 的 `OBS-11` 由「**两个计数相等**」改为「**白名单集合**」判据(`必在集 ⊆ 实际 ⊆ 必在集 ∪ 允许集 ∪ 运行期派生集`,差集点名打印),`nft` 同族缺陷一并按同一思路改造;改造后 **① 47 无活跃实例时 `OBS-11` 不再假红;② 「一进一出」替换式变化必须能被判红(假绿实证见 §6-E2)**;并给出在册 6 条小缺陷的清算排序(**只排序 + 最小验收 + 回滚,本单不修**)。
|
|||
|
|
|
|||
|
|
**本单不追求**:把 `OBS-09`(47 无活跃实例)判绿 —— 那是环境态,归 §5.9-Q3。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §2 只读前置(S0 必须先核实;任一条不符 ⇒ **停下报告**,⛔ 不硬闯)
|
|||
|
|
|
|||
|
|
| # | 事实 | 命令(本机 `cwd` = 工作区根;47 走 `ssh -p 22 bt-server`) | 期望输出 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **P1** | 47 监听口**原文全量**(判据形态的来源) | `ssh -p 22 bt-server "ss -lntp" > _中间产物_待清理/seq11/ss47-before.txt; wc -l < _中间产物_待清理/seq11/ss47-before.txt` | **77** 行(含表头)⇒ 76 socket;**每条都必须是 `地址:端口` 形态**(⛔ 见 P1-注) |
|
|||
|
|
| **P1-注** | 🔴 **形态风险(本单最容易踩)**:参数表的 `cleanValue()` 会**剥掉 `*` 与反引号**(`overlay-probe.cjs:68-70`)⇒ 白名单值里**绝不能出现 `*:443` 这种写法**,必须写 `ss` 实际打印的 `0.0.0.0:443` / `[::]:22`。**P1 的落盘原文就是唯一依据** | `grep -nE '\*' _中间产物_待清理/seq11/ss47-before.txt` | **零命中**(若有命中 ⇒ 停下报告,形态与 2026-09-17 的认知不同) |
|
|||
|
|
| **P2** | nft 判据的两条取数路径是否都可用 | `ssh -p 22 bt-server "nft --version; nft -j list ruleset \| head -c 200; echo; nft list ruleset \| wc -l"` | 版本行有输出|`-j` 回 JSON(首字符 `{`)|文本行数 = **72**。⚠️ **若 `-j` 不可用 ⇒ 不得放弃判据**,走文本退化路径并在输出里**显式标记**(§5-S2-d) |
|
|||
|
|
| **P3** | 参数表现取指纹(本单出单基线) | `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` | **`99e9e17b0c1ce0550e4bc7626a5a0494`** |
|
|||
|
|
| **P4** | 代码仓干净度(防越界) | `git -C D:/github/dsh_shenxian status --short \| wc -l; git -C D:/github/dsh_shenxian status --short src/ \| wc -l; md5sum D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | 总 **43**|`src/` 改动 **0**|探针 md5 现取并落 §8.9 |
|
|||
|
|
| **P5** | relay 端点**动态落点**(⛔ 它不是固定值,每次重启都会变) | `ssh -p 22 bt-server "curl -s http://127.0.0.1:20080/status" \| python3 -c "import sys,json;d=json.load(sys.stdin);print([e['localPort'] for e in d['endpoints']])"` | 形如 `[46147]`(序⑩ 现值 46147;⛔ 开工时**以现取为准**) |
|
|||
|
|
|
|||
|
|
> ⚠️ `_中间产物_待清理/seq11/` 是本单的取证落盘目录(先 `mkdir -p`)。收口时**保留**(供第三方复核),⛔ 不要写进正式档案。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §3 范围
|
|||
|
|
|
|||
|
|
**改(3 类,全部是"观测面",⛔ 无一在生产路径上)**
|
|||
|
|
|
|||
|
|
| # | 文件 | 改动性质 | 是否计入本单前置指纹 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 1 | `D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | `OBS-11` 判据主体 + nft 同族改造 + 取数改为回传原文 | 代码,**不进**参数表指纹 |
|
|||
|
|
| 2 | `参数表_覆盖网络_20260917.md` §6(新增集合键、退役计数键)+ §10 | 新增 4 键 / 退役 2 键说明 | **会变** ⇒ §5-S5 必须重取并记 §8.9 |
|
|||
|
|
| 3 | 工作区根 `接续入口_覆盖网络线_20260916.md` §0(1 行) | 口径指针 | 入口无指纹口径 |
|
|||
|
|
|
|||
|
|
**⛔ 不动(明确写死,防顺手扩大)**
|
|||
|
|
|
|||
|
|
- ⛔ **`src/**` 一字不改**(本单是观测面改造;`git status --short src/ | wc -l` 必须仍 = **0**)
|
|||
|
|
- ⛔ 不动任何生产阈值:`RELAY_FAILOVER_*` / `HB_SEC` / burst 语义 / `RELAY_MAX_HOSTS` / `MemoryHigh`·`MemoryMax`
|
|||
|
|
- ⛔ 不开新监听口 / 不新增凭据 / 不放宽 nft / 不碰 nginx / 不重启 `dshs` 或 relay(本单**无生产中断**)
|
|||
|
|
- ⛔ **不 commit / 不 push**(未获授权)
|
|||
|
|
- ⛔ 不回去改已收官的历史单(`交接单_检测时延…md` 等记录的是**当时实况**;新口径以参数表为单一来源 ⇒ §5-S5 只在参数表内写"退役说明",**不追改历史单**)
|
|||
|
|
- ⛔ **不做**:presence / 房间层 / 内容分发(清单第 7 步)|106 agent 面吃引导链(已判不执行)|worker 拨出方向、relay 自研形态、打洞实现|同机候选去重 / 并发竞速 open(已判不做)|第 4/5 台真机(已拍板 = 本机多实例)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §4 决策点
|
|||
|
|
|
|||
|
|
**已定(本规划棒自决,可推翻)**
|
|||
|
|
|
|||
|
|
1. **判据形态 = 三集包含式集合判据**(不是"计数 + 容差",也不是"只报不判"):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
required ⊆ actual // 必在集:缺一条 ⇒ FAIL(点名缺项)
|
|||
|
|
actual ⊆ required ∪ allowed ∪ derived
|
|||
|
|
// 多一条 ⇒ FAIL(点名多出项)
|
|||
|
|
derived = { 127.0.0.1:<p> | p ∈ /status.endpoints[].localPort } // 唯一来源 = relay 自身 /status
|
|||
|
|
allowed = 表中静态允许集(含区间,见 §5-S1)
|
|||
|
|
```
|
|||
|
|
> 依据:`required` 抓"**消失**"、包含式抓"**新增**"、`derived` 让**合法动态落点**有名字 ⇒ 替换式变化(一进一出)**必然**被两条断言之一命中。
|
|||
|
|
|
|||
|
|
2. **`LISTEN_COUNT` 的"有/无实例态"两值 —— ⛔ 不表达,直接消除**:实例档 `20000`(w-47)/ `21000`(w-106)进**允许区间** ⇒ 有实例则出现(合法)、无实例则不出现(不红)⇒ 状态无关化。
|
|||
|
|
⛔ **不新增** `LISTEN_COUNT_UP` / `LISTEN_COUNT_DOWN`(那等于把"对状态敏感"这条缺陷**编码进参数表** = 净退化,违 **R11**)。
|
|||
|
|
3. **`nft` 同族缺陷一并改造(结论:改,但机制与监听口不同)**:nft 是**内容**不是端口 ⇒ 用 `nft -j` **入站 accept 集合 ⊆ 白名单**(语义判据,比行数更贴近"零新增暴露面"的本意);`nft list ruleset | wc -l` **降级为"仅打印的上下文"**,不再参与判 PASS/FAIL。⛔ 退化为文本解析时**必须显式标记**,⛔ 不得静默改判据(`text-fallback` 字样进输出行)。
|
|||
|
|
4. **`LISTEN_COUNT` / `NFT_RULES` 退役为"历史留档"**:两行**留在**参数表(供对账),但 `OBS-11` 判据列**不再引用**;退役说明写在参数表 §6 内(单一来源内自解释),⛔ 不追改历史单。
|
|||
|
|
5. **假绿/假红的**自证**方式 = 夹具 + 真机受控临时口**(§5-S3 / §6-E2、E3)—— 判据改造**必须自带"旧判据会放过、新判据能抓住"的实证**,否则等于换了个说法。
|
|||
|
|
|
|||
|
|
**硬门(三条,⛔ 任一违反即停手)**
|
|||
|
|
|
|||
|
|
- **D1**:分解类前提未复现前 ⛔ **不许改任何生产值**(本单只改观测面,本就不该碰生产值 ⇒ 违反即意味着范围外扩)
|
|||
|
|
- **R7**:范围外缺陷**先报告、不动手**(§5.9 的 6 条里,本单**只执行 Q6 的"结构化吸收"**,其余全部只排序)
|
|||
|
|
- 🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**(该值已判「看似合法、实际自锁」:`num()` 的 `/^\d+$/` 放行 `'0'`,但归零会让"失败候选必须被排除"失效 ⇒ 候选链卡死,实测 **121–123 s 无切换**;序⑩ 已把 `--scene 4c` / `ctrl` 整体移除并**显式拒绝**)。本单**不涉及任何切流演练** ⇒ 全单不该出现这个键。
|
|||
|
|
|
|||
|
|
**§4.3 待你拍板 = 空**(本单**零上抛**;§5.10 的范围外清单只登记、不定序)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §5 步骤
|
|||
|
|
|
|||
|
|
### S0 · 只读前置
|
|||
|
|
照 §2 跑完 P1–P5。**任一条与期望不符 ⇒ 停下报告**(附原文)。⚠️ P1 与 P1-注 是**全部设计的形态依据**,必须先落盘再动手。
|
|||
|
|
|
|||
|
|
### S1 · 定义白名单集合(改**参数表** §6,⛔ 值一律按 P1 原文写)
|
|||
|
|
|
|||
|
|
新增 4 键(全部加在 §6 阈值表内,`KEY_RE` 能解析;⛔ 值内不得含 `|`、`*`、反引号):
|
|||
|
|
|
|||
|
|
| 键 | 含义 | 值形态(示例;**实际以 P1 原文为准**) |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `LISTEN_REQUIRED` | **必在集**:缺失 ⇒ FAIL。口径 = **平台工作必需 且 不受实例/端点在线态影响**的固定口 | 逗号分隔的 `地址:端口`,如 `0.0.0.0:22,[::]:22,0.0.0.0:80,0.0.0.0:443,0.0.0.0:888,0.0.0.0:3080,127.0.0.1:20080` |
|
|||
|
|
| `LISTEN_ALLOWED` | **允许集(非必在)**:出现合法、消失不红 | 同上形态,如 `0.0.0.0:58888,127.0.0.1:8765,127.0.0.1:15432,127.0.0.1:19100` |
|
|||
|
|
| `LISTEN_ALLOWED_RANGES` | **区间允许集**(实例档 / 拨号池) | `127.0.0.1:20000-20099,127.0.0.1:21000-21099`(两段取自序③ P3 的实例端口区间隔离)。⚠️**拨号池不在此键内** —— 复用表内既有 `DIAL_POOL_BOUND`(`25000–25063`)派生,⛔ 不重复写死(避免两处漂移) |
|
|||
|
|
| `NFT_ALLOW_INBOUND` | **入站 accept 白名单**(S4 用) | `tcp:22,tcp:80,tcp:443,tcp:888,tcp:3080,tcp:58888`(+ 无端口匹配的必要规则另立一个标记位,形态见 S4) |
|
|||
|
|
|
|||
|
|
**成员归属的判据(写进参数表备注,防下一棒再吵)**:
|
|||
|
|
- **必在** = 它不在 ⇒ **平台本身坏了**(sshd / nginx 80·443 / 宝塔 888 / 平台门户 3080 / relay `20080`)
|
|||
|
|
- **允许** = 它不在 ⇒ 只是**某个可选项没开**(运维面板 58888、管理 UI 8765、PG 15432、Worker 19100、实例档、拨号池)
|
|||
|
|
> ⚠️ 若 P1 原文显示某口与上表推断不符(例如 `888` 实际不在监听)⇒ **以原文为准**,并把该口从 `LISTEN_REQUIRED` 移到 `LISTEN_ALLOWED`(本单不追求"凑齐九个")。
|
|||
|
|
|
|||
|
|
### S2 · 改探针 `scripts/overlay-probe.cjs`(本单唯一代码改动)
|
|||
|
|
|
|||
|
|
**a. 取数:远端只回传原文,归一化在本地做**
|
|||
|
|
- 现状 `listenTotal=$(ss -lntp | wc -l)` ⇒ 改为**回传原文**:`echo "listenRaw=$(ss -lntp | base64 -w0)"`(单行、`base64 -w0` 字母表无 `=` 除末尾填充 ⇒ 与现有 `key=value` 解析兼容)。
|
|||
|
|
- nft 同法:`echo "nftRaw=$(nft -j list ruleset | base64 -w0; echo <退化标记>)"` —— ⛔ **`-j` 失败必须能分辨**(见 d)。
|
|||
|
|
- ⛔ **仍只准一次 ssh**(现有 `remoteFacts()` 一条命令取全部事实的设计不许破)。
|
|||
|
|
|
|||
|
|
**b. 归一化(本地)**
|
|||
|
|
- `ss` 行:跳过表头 / 空行 ⇒ 按 `\s+` 切分取 **Local Address:Port** 列 ⇒ 从**右侧**第一个 `:` 拆 host/port(`[::]:22` 必须拆对,⛔ 不能用左侧 `indexOf`)。
|
|||
|
|
- 产出集合 `actual`,**逐条保留原文字符串**(报错要点名)。
|
|||
|
|
|
|||
|
|
**c. 判据(替换 `overlay-probe.cjs:251-259` 的 `OBS-11`)**
|
|||
|
|
```
|
|||
|
|
missing = required \ actual // 缺项 ⇒ FAIL
|
|||
|
|
extra = actual \ (required ∪ allowed ∪ ranges ∪ derived) // 多出 ⇒ FAIL
|
|||
|
|
ok = missing.length === 0 && extra.length === 0 && relayListenTotal === relayListenLoopback && relayListenTotal > 0
|
|||
|
|
```
|
|||
|
|
- 输出行(≤12 行的约束不许破):`OBS-11 集合 必在 n/7 允许 m 区间 k 派生 d 实际 t 多出 x 缺失 y`
|
|||
|
|
- 差集**逐条点名**写到 stderr:`OBS-11 多出 127.0.0.1:9999` / `OBS-11 缺失 0.0.0.0:443`
|
|||
|
|
- ⛔ **`LISTEN_COUNT` / `NFT_RULES` 不再被 `r.num()` 读取**(退役;两行留在表内仅供对账)
|
|||
|
|
|
|||
|
|
**d. nft(S4 的实现细节,与 c 同批落地)**
|
|||
|
|
- `nft -j` 可用 ⇒ 解析 JSON:取 **`input` 链**(family `ip`/`ip6` 的 `filter`/`inet` 视 P2 原文)中 **verdict 含 accept** 的规则 ⇒ 归一为 `{proto}:{dport}`(无端口匹配的规则归为专用标记,如 `established`)。
|
|||
|
|
- `nft -j` 不可用 ⇒ `nft list ruleset` 文本按行抽 `accept` 行 ⇒ **同一判据**,但输出行尾加 `nft=text-fallback`(⛔ 不许静默)。
|
|||
|
|
- 判据 = `acceptSet ⊆ NFT_ALLOW_INBOUND`;多出逐条点名(`OBS-11 nft 多出 tcp:9999`)。
|
|||
|
|
|
|||
|
|
**e. 零数字纪律(判据见 §6-E5)**:新增代码**不得出现任何 ≥3 位的数字字面量**(端口/池/日期全部走参数表或从取回数据里取)⇒ `grep -nE "[0-9]{3,}" scripts/overlay-probe.cjs` 仍须**零命中**。
|
|||
|
|
|
|||
|
|
### S3 · 假红 / 假绿的自证(**本单的核心验收,⛔ 不许省**)
|
|||
|
|
|
|||
|
|
**a. 夹具模式(离线、零生产副作用)**:给探针加两个**仅测试用** argv:`--listen-fixture <file>` / `--nft-fixture <file>`(文件内容 = 远端原文,即 `ss -lntp` / `nft -j` 的输出)。
|
|||
|
|
- 夹具模式下**不 ssh**;输出行首加 `⚠️ FIXTURE` 标记,**stderr 同时标一次**(⛔ 防止被下游当成生产结论)。
|
|||
|
|
- 退出码仍按判据给 `0/1`(否则先红后绿无法表达)。
|
|||
|
|
- ⛔ 夹具模式**必须配 `--table <副本>`**(避免误改生产参数表)。
|
|||
|
|
|
|||
|
|
**b. 三份夹具 + 新旧判据对照表**(落盘 `_中间产物_待清理/seq11/fx-*.txt`,三份都从 P1 原文**机械改写**,⛔ 不手搓)
|
|||
|
|
|
|||
|
|
| 夹具 | 构造 | 旧判据(计数相等) | 新判据(集合) | 证的是 |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| `fx-normal.txt` | P1 原文 | FAIL(77≠79 ⇒ **假红**) | **PASS** | ⓐ 假红已消除 |
|
|||
|
|
| `fx-swap.txt` | 原文**去掉一行**(如 `0.0.0.0:888`)**加一行**(`127.0.0.1:9999`)⇒ **行数不变** | **PASS(假绿!)** | **FAIL**(多出 `9999` + 缺失 `888`,各点名) | ⓑ 替换式变化已能抓住 |
|
|||
|
|
| `fx-instance.txt` | 原文**加上** `127.0.0.1:20000`(模拟 47 有活跃实例) | FAIL(78≠79 ⇒ 假红) | **PASS** | 实例态不再影响判据 |
|
|||
|
|
|
|||
|
|
**c. 真机正向复验(47,只读 + 一个受控临时口)**
|
|||
|
|
1. 现态跑一次 ⇒ 期望 **`OBS-11 PASS`**,且 stderr 无差集(`overlay-probe` 总数由 **10/12 → 11/12**,剩余的 `OBS-09` = 环境态)。
|
|||
|
|
2. **受控临时口**:本机 `ssh -p 22 bt-server "python3 -c '…'"` 起一个**只绑 `127.0.0.1` 的临时监听口**(口值从 47 的空闲回环口里现取,⛔ 不猜、⛔ 不绑公网)⇒ 新判据**必须 FAIL 并点名该口**;随即**关掉** ⇒ 必须回 PASS。
|
|||
|
|
⚠️ 该临时口的值**必须记进 §8**(自证"多出必红"用的就是它);⚠️ 用 `timeout`/`trap` 保证**即使中断也会自动关闭**(⛔ 不留残留监听口)。
|
|||
|
|
|
|||
|
|
### S4 · nft 集合判据落表
|
|||
|
|
按 S1 的 `NFT_ALLOW_INBOUND` 定值(**以 P2 的 `nft -j` 原文为准**,⛔ 不抄本单示例)。当前 `nft list ruleset | wc -l` = **72** ⇒ 归一后入站 accept 项数应远小于 72(含 `established`/`related` 一类无端口规则)。⚠️ 若发现**白名单写不全**(如 `icmp` 或 `ct state` 规则)⇒ 补白名单**而不是**放松判据;若确实存在"说不清来源的入站 accept"(如宝塔自行插入)⇒ **记进 §8.8 的"在册未办"并点名命令**,⛔ 不许把它静默加进白名单。
|
|||
|
|
|
|||
|
|
### S5 · 连带更新
|
|||
|
|
1. **参数表 §6**:新增 4 键;`LISTEN_COUNT` / `NFT_RULES` 两行后加注「⛔ **已退役(序⑪ 集合判据替代)—— 仅对账用,不得再作为判据**」(值**保留**,历史可比)。
|
|||
|
|
2. **参数表 §10 指纹**:按表内既有口径重取现值,写进 §10 的"现取"行。
|
|||
|
|
3. **入口 `接续入口_覆盖网络线_20260916.md` §0**:加/改 1 行口径指针(指向本单 §8.9 与参数表 §6)。
|
|||
|
|
4. ⛔ **不追改历史单**(见 §3)。
|
|||
|
|
|
|||
|
|
### S6 · 收口
|
|||
|
|
按 §7 回滚准备 → 释放锁 → 用**陈述句**告知接续并登记下一棒(`scheduledAt` = 收口 + 2~5 分钟;⚠️ 下一棒 id **只能来自工具返回值**,先 create、再取 id、再落盘)→ 写工作区日志 → §8 回填。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### §5.9 在册小缺陷清算排序(② · **本单只排序 + 给最小验收与回滚,⛔ 不修**)
|
|||
|
|
|
|||
|
|
| 序 | 缺陷 | 类型 | 最小验收(命令 ⇒ 期望) | 回滚 | 依赖 |
|
|||
|
|
|---|---|---|---|---|---|
|
|||
|
|
| **Q1** | `mksess*.cjs` 失效(`/opt/dshs/mksess*.cjs` 仍写 **SQLite 旧库**) | 工具链(**阻塞验收手段**) | 用它开一次临时 session ⇒ 实例面 `curl -w '%{http_code}'` 回 **200/401**(⛔ 非 `000`);用完即删 | 恢复原脚本 + 删 session | 无(**先做**) |
|
|||
|
|
| **Q2** | guest(w-106) 实例页 **502** | 生产缺陷(用户可见) | 同 Q1 的临时 session ⇒ w-106 实例页 **200** | `cp -a <lib 备份>/lib/. /opt/dshs/lib/` → `systemctl restart dshs` | **Q1**(没有临时 session ⇒ 无法验收) |
|
|||
|
|
| **Q3** | `OBS-09` 实例面 `000`(47 无活跃实例) | **环境态,非缺陷** | 47 起活跃实例 ⇒ `OBS-09` 绿 **且** 新 `OBS-11` 在"有实例态"下仍绿(`20000` 落在允许区间) | 停实例(回到现态) | **Q2**(同批做最省) |
|
|||
|
|
| **Q4** | `src/net/relay/**` **untracked 留档缺口** | 交付基线(**需用户授权**) | `git ls-files src/net/relay \| wc -l` > 0 且与工作区一致 | 不适用(只增不改) | **用户授权**(§4 提交边界 ⇒ 只报告) |
|
|||
|
|
| **Q5** | `client.ts.bak-seq7-*` 残留 | 清理 | `ls src/net/relay/*.bak-*` 为空 **且** `npm test` 值不变(基线 160/159/0/1) | 从同目录备份/产物重生成(⚠️ 删前先确认 lib 内已有等价产物) | 无 |
|
|||
|
|
| **Q6** | 参数表 `LISTEN_COUNT`=79 与现态的 **1 条无名口** | 观测口径 | **被 S1–S3 结构化吸收**(集合判据不再依赖计数);剩余 = 47 恢复活跃实例后复取应回 **78 行** | 参数表备份恢复(指纹回 `99e9e17b…`) | **本单即办**;"78 行"的复核与 Q3 合批 |
|
|||
|
|
|
|||
|
|
**排序理由(一句话)**:**Q1 是"验收手段"**(R4 规定实例面验收一律走临时 session)⇒ 它不修,Q2/Q3 连"做完了没有"都判不了;**Q2 是唯一用户可见的坏**(502);**Q3 是环境态**且与 Q2 同批最省;**Q4 卡在授权**(不在技术侧);Q5/Q6 是收尾。
|
|||
|
|
**本单实际动手的只有 Q6 的"吸收"部分**(属 S1–S3 的副产品);⚠️ 其余 5 条 **⛔ 本单不修**(R7),执行会话把它们**原样转给下一棒**。
|
|||
|
|
|
|||
|
|
### §5.10 范围外(⛔ 只登记,本单**不定序、不上抛**)
|
|||
|
|
|
|||
|
|
presence / 房间层 / 内容分发(清单第 7 步)|106 agent 面吃引导链(已判不执行)|worker 只拨出 / relay 自研 / 不做打洞实现|同机候选去重 / 并发竞速 open(已判不做)|第 4/5 台真机 = 本机多实例(已拍板)。
|
|||
|
|
> ⛔ 这些的**先后顺序属"业务优先级"**,按 §1 判据属边界外 ⇒ **本单只登记、不排序、不决策**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §6 验收(命令 + 期望输出 + 退出码;可被第三方复现)
|
|||
|
|
|
|||
|
|
| # | 项 | 命令(`cwd` = 工作区根) | 期望 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **E1** | 新判据在真实态全绿 | `node "D:/github/dsh_shenxian/scripts/overlay-probe.cjs"; echo "rc=$?"` | `PASS OBS-11 …`(stderr 无差集);**10/12 → 11/12**(剩 `OBS-09` 环境态);`rc=1`(因 OBS-09) |
|
|||
|
|
| **E2** | **假绿实证**(本单核心) | `node …/overlay-probe.cjs --table <副本> --listen-fixture _中间产物_待清理/seq11/fx-swap.txt` | **旧判据 PASS / 新判据 FAIL**,且 stderr 同时点名**多出**与**缺失**各 1 条(对照表全文进 §8) |
|
|||
|
|
| **E3** | 真机受控临时口 | S3-c 两步(起口 ⇒ 跑探针 ⇒ 关口 ⇒ 再跑) | 起口后 `FAIL` 且点名该口;关口后回 `PASS`;**收口时该口必须已关闭**(`ss -lntp` 复核 77 行) |
|
|||
|
|
| **E4** | nft 判据两路可用 | `ssh -p 22 bt-server "nft -j list ruleset \| head -c 50"` + 一次探针 | `acceptSet ⊆ NFT_ALLOW_INBOUND`;走退化路径时输出含 `text-fallback` |
|
|||
|
|
| **E5** | 零数字纪律未被破 | `grep -nE "[0-9]{3,}" D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | **零命中**(退出码 1) |
|
|||
|
|
| **E6** | 零回归(⛔ 四个都验) | `npm.cmd test`(Node 22)|`node …/overlay-failover-drill.cjs --scene all`|`ssh -p 22 bt-server "ss -lntp \| wc -l; nft list ruleset \| wc -l"` | 测试 **≥ 160/159/0/1**|`--scene all` **12 PASS / 0 FAIL**|`77` / `72`(本单不产生新监听口) |
|
|||
|
|
| **E7** | 越界自证 | `git -C D:/github/dsh_shenxian status --short src/ \| wc -l; git -C D:/github/dsh_shenxian status --short \| wc -l` | `src/` = **0**;总数 = **43 + 本单新增**(⛔ 无 `src/` 改动、无 `.bak` 残留) |
|
|||
|
|
| **E8** | 回滚可用 | §7 的恢复命令 + 重跑探针 | 回到 **10/12**(`OBS-11` 重新红) |
|
|||
|
|
| **E9** | 参数表指纹已更新并记录 | `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` | 与 §8.9 记录值一致(⛔ 不得仍等于出单基线 `99e9e17b…`) |
|
|||
|
|
|
|||
|
|
**退出码约定**(沿用探针原设计,⛔ 不改语义):全绿 `0` / 任一红 `1` / 用法或取数失败 `2`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §7 回滚(全部秒级,⛔ 无生产中断)
|
|||
|
|
|
|||
|
|
| 层 | 改前准备 | 回滚命令 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 探针 | `cp -a D:/github/dsh_shenxian/scripts/overlay-probe.cjs _中间产物_待清理/seq11/overlay-probe.cjs.bak-<ts>`(⛔ **不进 `src/`**,收口时**删除仓内任何 `.bak`**) | `cp -a _中间产物_待清理/seq11/overlay-probe.cjs.bak-<ts> D:/github/dsh_shenxian/scripts/overlay-probe.cjs` |
|
|||
|
|
| 参数表 | `cp -a 参数表_覆盖网络_20260917.md _中间产物_待清理/seq11/参数表.bak-<ts>` | 拷回 ⇒ 复核指纹回到 **`99e9e17b0c1ce0550e4bc7626a5a0494`** |
|
|||
|
|
| 入口 | 只加 1 行 ⇒ 手工删该行 | 同左 |
|
|||
|
|
| 临时监听口 | §5-S3-c 的 `timeout`/`trap` 兜底 | `ssh -p 22 bt-server "ss -lntp \| wc -l"` 必须回 **77** |
|
|||
|
|
|
|||
|
|
- ⛔ 本单**不重启** `dshs` / relay ⇒ 无在线用户影响;⛔ 回滚路径**不含任何** `RELAY_FAILOVER_*` 值(尤其 ⛔ 不含 `…COOLDOWN_MS=0`)。
|
|||
|
|
- ⛔ **`.bak` 只在执行期存在**:验收全绿后**立即删除**仓内 `.bak`(`client.ts.bak-seq7-*` 残留就是反例 ⇒ 本单顺手立规矩,并把结果写进 §8)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §8 回报格式(执行会话按此回填;⛔ 缺一节即算未完成)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
### 8.1 结论(1 行) :OBS-11 集合判据是否落地 / E1–E9 逐条 PASS·FAIL
|
|||
|
|
### 8.2 只读前置 P1–P5 :逐条贴命令与输出(P1 的 77 行原文落盘路径 + 形态核对)
|
|||
|
|
### 8.3 白名单定值 :LISTEN_REQUIRED / ALLOWED / ALLOWED_RANGES / NFT_ALLOW_INBOUND 四个键的**最终值**(含"为什么是这几个")
|
|||
|
|
### 8.4 假红假绿实证 :E2 的三行对照表(夹具 ⇒ 旧判据 / 新判据)+ 三份夹具的 md5
|
|||
|
|
### 8.5 真机正向复验 :E1 / E3 的命令原文与输出(含临时口的**口值**与"关闭后已复原"证据)
|
|||
|
|
### 8.6 nft 判据 :`-j` 是否可用、归一后的 accept 集合、与白名单的关系(⚠️ 若有"说不清来源"的项 ⇒ 点名并转 §8.8)
|
|||
|
|
### 8.7 零回归 / 越界自证 :E5–E7 原文(npm test 计数、--scene all 计数、ss/nft 计数、git status 计数、src=0)
|
|||
|
|
### 8.8 未过项 / 在册未办 :逐条写「现象 / 已做到哪一步 / 回头条件」;§5.9 的 Q1–Q5 原样转下一棒
|
|||
|
|
### 8.9 指纹(可复现核对口径)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### §8.9 指纹
|
|||
|
|
|
|||
|
|
- **参数表**:`sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum` ⇒ 出单基线 **`99e9e17b0c1ce0550e4bc7626a5a0494`**(执行后**必须已变**并在此回填现值)
|
|||
|
|
- **本交接单前缀(§8 及其后不计入)**:`sed '/^## §8 回报格式/,$d' 交接单_观测口径与在册缺陷_20260917.md | md5sum` ⇒ 现取值**由出单棒在入口与本棒回复中登记**(⛔ 本文件内**不自嵌**,避免自指)
|
|||
|
|
- **交叉证据(收口复取)**:`交接单_检测时延与deadline_20260917.md` = **`d903b4eeabf25ef381379cdbaac77e8a`**(§8 前缀口径)|`参数表` 出单基线见上
|
|||
|
|
- **原始取证落盘**:`_中间产物_待清理/seq11/`(`ss47-before.txt` = P1 原文;`fx-normal.txt` / `fx-swap.txt` / `fx-instance.txt` = 三份夹具;`overlay-probe.cjs.bak-<ts>` = 探针回滚副本)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §8 执行回报(序⑫ 执行棒 · 2026-09-17 15:0x–15:2x)
|
|||
|
|
|
|||
|
|
> ⚠️ 本回报**追加在 §8 之后** ⇒ **§8 前缀指纹 `3ece0f87…` 未变**(回填不进入前缀,与序⑨/⑩ 同例)。
|
|||
|
|
> ⚠️ 单头「状态:⏳ 待执行」一行**保持原样不动** —— 改它会动前缀指纹;本棒状态以 §8.1 与本行下方为准(**已执行完毕**)。
|
|||
|
|
|
|||
|
|
### 8.1 结论(1 行)
|
|||
|
|
|
|||
|
|
**✅ `OBS-11` 集合判据已落地;E1–E9 = 9 全绿**(其中 **E6-2 在第 3 次尝试才通过** —— 前两次被 **106 的 `sshd` MaxStartups 限流**中止,属**环境级、非本单引入**,已入册 **§8.8-1**)。`overlay-probe` **10/12 → 11/12**(剩 `OBS-09` = 环境态);**假红已消除 + 假绿已能抓**(§8.4 有对照表)。
|
|||
|
|
|
|||
|
|
### 8.2 只读前置 P1–P5
|
|||
|
|
|
|||
|
|
| # | 命令(`cwd` = 工作区根) | 实测 | 期望 | 判定 |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| **P1** | `ssh -p 22 bt-server "ss -lntp" > …/seq11/ss47-before.txt 2>/dev/null; wc -l < …` | **77 行**(=表头 1 + socket **76**) | 77 | ✅ |
|
|||
|
|
| **P1-注** | `awk 'NR>1 && $1=="LISTEN"{print $4}' … \| grep -c '\*'` | **0** | 0 | ✅(⛔ 按**实质判据**;见 §8.8-2-①) |
|
|||
|
|
| **P2** | `ssh -p 22 bt-server "nft --version; nft -j list ruleset \| head -c 120; … nft list ruleset \| wc -l"` | `nftables v1.0.4`|`-j` **回 JSON**(首字符 `{`)|文本 **72 行** | 同 | ✅ |
|
|||
|
|
| **P3** | `sed '/^## §10 指纹/,$d' 参数表… \| md5sum` | **`99e9e17b0c1ce0550e4bc7626a5a0494`** | 同 | ✅ |
|
|||
|
|
| **P4** | `git -C D:/github/dsh_shenxian status --short \| wc -l`;`… src/ \| wc -l` | 总 **43**|`src/` **20** | 43|**0** | 总 ✅;`src/` ⛔ 期望值本身有误 ⇒ 见 §8.8-2-② |
|
|||
|
|
| **P5** | `ssh -p 22 bt-server "curl -s 127.0.0.1:20080/status"` → 取 `endpoints[].localPort` | **`[40985]`**(与 relay `20080` 同 PID 781675) | 现取为准 | ✅(⚠️ 动态值,序⑩ 的 46147 已过期 —— 印证"⛔ 不能写死") |
|
|||
|
|
|
|||
|
|
> **P1 原文的 76 条监听口分类**(这是白名单定值的**唯一依据**):
|
|||
|
|
> `0.0.0.0:22` / `[::]:22` / `0.0.0.0:80` / `0.0.0.0:443` / `0.0.0.0:888` / `0.0.0.0:8765` / `0.0.0.0:58888` / `127.0.0.1:3080` / `127.0.0.1:15432` / `127.0.0.1:19100` / `127.0.0.1:20080` / `127.0.0.1:40985` + **`127.0.0.1:25000–25063`(64 口,拨号池)**。
|
|||
|
|
> ⇒ `7 必在 + 4 允许 + 1 派生 + 64 拨号池 = 76` **逐条有名、零无名口**(对账闭合)。
|
|||
|
|
|
|||
|
|
### 8.3 白名单定值(四个键的最终值 + 为什么是这几个)
|
|||
|
|
|
|||
|
|
| 键 | 最终值 | 为什么是这几个 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `LISTEN_REQUIRED` | `0.0.0.0:22,[::]:22,0.0.0.0:80,0.0.0.0:443,0.0.0.0:888,127.0.0.1:3080,127.0.0.1:20080` | 判据 = **它不在 ⇒ 平台本身坏了**(sshd/nginx `80`·`443`/宝塔 `888`/平台门户 `3080`/relay 回环口)。⚠️ 与单内示例的**唯一差异** = **`3080` 写在 `127.0.0.1` 上**(47 实测绑回环,⛔ 不是 `0.0.0.0:3080`)—— 单内示例写错,按 P1 原文为准 |
|
|||
|
|
| `LISTEN_ALLOWED` | `0.0.0.0:58888,0.0.0.0:8765,127.0.0.1:15432,127.0.0.1:19100` | 判据 = **它不在 ⇒ 只是某个可选项没开**(宝塔面板口/管理 UI/控制面 PG/本机 worker agent)⇒ 出现合法、消失**不红** |
|
|||
|
|
| `LISTEN_ALLOWED_RANGES` | `127.0.0.1:20000-20999,127.0.0.1:21000-21999` | 两台 worker 的**实例端口区间**。🔴 **span 取 1000,不是单内示例的 100** —— 实测 47 的 `dshs-worker.service`(经 `/proc/<pid>/environ`)与 106 的 `/etc/dshs-worker.env` 均为 `DSHS_INSTANCE_PORT_BASE=20000`/`=21000` + **`DSHS_INSTANCE_PORT_SPAN=1000`**;并集 `[20000,22000)` 正好落在 relay 声明窗口 `[19000,22000)`(`--base 19000 --span 3000`)**之内** ⇒ 自洽。⛔ 拨号池**不在此键内**(复用表内既有 `DIAL_POOL_BOUND` 派生,避免两处漂移) |
|
|||
|
|
| `NFT_ALLOW_INBOUND` | `tcp:22,tcp:80,tcp:443,tcp:888,tcp:3080,tcp:58888` | 入站 accept 的**语义白名单**(见 §8.6)。⚠️ 归一形态 = `<proto>:<dport>`;「无 dport 匹配的 accept」归一为 `<proto>:any` —— **当前 47 的 input 链 0 条规则 ⇒ 集合为 ∅、本键暂无标记位**(将来若出现必须显式加进来,⛔ 不许放宽判据) |
|
|||
|
|
|
|||
|
|
**"有/无实例态"的处置(⭐ 本单的结构性决定)**:⛔ **不新增** `LISTEN_COUNT_UP` / `LISTEN_COUNT_DOWN` 两个计数(那等于把"对状态敏感"编码进单一来源 = 净退化,违 **R11**)⇒ 把实例档 `20000` / `21000` 直接放进**允许区间**:**出现不报、消失不红**,状态敏感性问题被**消除**而不是被打补丁。
|
|||
|
|
|
|||
|
|
### 8.4 假红假绿实证(E2 核心)
|
|||
|
|
|
|||
|
|
**判据口径先说清**(⛔ 否则数字会被误读):**旧判据口径** = `ss -lntp | wc -l`(**含表头**,比 `LISTEN_COUNT`=79);**新判据口径** = **socket 集合大小**(不含表头)。三份夹具**全部由 P1 原文机械改写**(`mk-fixtures.py`,⛔ 不手搓):
|
|||
|
|
|
|||
|
|
| 夹具 | 构造 | 行数 | **旧判据** | **新判据** | 证的是 |
|
|||
|
|
|---|---|---|---|---|---|
|
|||
|
|
| `fx-normal.txt` | P1 原文**逐字** | 77 | **FAIL**(`77 ≠ 79`)⇒ **假红** | **PASS**(多出 0 缺失 0) | ⓐ 假红已消除 |
|
|||
|
|
| `fx-swap.txt` | 原文 **+ `20000` + `21000`**(模拟两实例活跃 ⇒ 行数回到 79)**− `0.0.0.0:888` + `127.0.0.1:9999`**(一进一出) | **79** | **PASS(假绿!)** | **FAIL** —— 同时点名 `OBS-11 多出 127.0.0.1:9999` + `OBS-11 缺失 0.0.0.0:888` | ⓑ 替换式变化已能抓住 |
|
|||
|
|
| `fx-instance.txt` | 原文 **+ `127.0.0.1:20000`** | 78 | **FAIL**(`78 ≠ 79`)⇒ **假红** | **PASS** | 实例态不再影响判据 |
|
|||
|
|
| `fx-nft-extra.json` | `nft -j` 原文 + 一条 `INPUT` 上的 `accept tcp dport 9999` | — | — | **FAIL** —— 点名 `OBS-11 nft 多出 tcp:9999` | nft 侧"多出必红" |
|
|||
|
|
| `nft-text.txt` | `nft list ruleset` **文本原文**(喂给 `--nft-fixture` ⇒ 必走退化路径) | 72 | 旧口径行数相等 ⇒ PASS | **PASS** + 输出含 **`nft=text-fallback`**;accept 集合 = **∅**(`FORWARD` 的 accept **未误抓** ⇒ 链感知有效) | 退化路径**同一判据**、⛔ 不静默 |
|
|||
|
|
| `fx-nft-text-extra.txt` | 上述文本 + 在 `chain INPUT {` 后插一行 `tcp dport 9999 counter accept` | 73 | — | **FAIL** —— 点名 `tcp:9999` | 退化路径同样"多出必红" |
|
|||
|
|
|
|||
|
|
**三份 ss 夹具 md5**:`fx-normal.txt` = `e53031f7f2485b4609a69128effb7210`|`fx-swap.txt` = `dadd94f85df9a2c1e115afce17bfd91e`|`fx-instance.txt` = `45cee07da44d5c3fa4a92475f03be2ed`
|
|||
|
|
**入场码**:`fx-normal` rc=**0**|`fx-swap` rc=**1**|`fx-instance` rc=**0**|`fx-nft-extra` rc=**1**|`nft-text` rc=**0**|`fx-nft-text-extra` rc=**1**(⛔ 夹具模式退出码仍按判据给,否则"先红后绿"表达不出来)
|
|||
|
|
|
|||
|
|
⚠️ **与单内 §6-E2 的一处构造差异(必须记)**:单内写「`fx-swap` = 原文**去掉一行加一行**、**行数不变**」并同时期望「旧判据 PASS」。**这两条指不并存** —— 若行数仍 = 77,则旧判据也 FAIL ⇒ **假绿证不出来**。本棒按**实质判据**构造:先补两个实例口把行数**抬到 79(= `LISTEN_COUNT`)**,再做一进一出 ⇒ 两条期望**同时**满足(旧 PASS/新 FAIL)。这是"让判据可被证伪",⛔ 不是改判据。
|
|||
|
|
|
|||
|
|
### 8.5 真机正向复验(E1 / E3)
|
|||
|
|
|
|||
|
|
**E1(真实态)**:`node scripts/overlay-probe.cjs` ⇒ `rc=1`(因 `OBS-09`)|**PASS 11 / FAIL 1**(唯一红 = `OBS-09 实例面 本机:20000=000 对端:21000=000`)|`OBS-11` 原文:
|
|||
|
|
`PASS OBS-11 集合 必在 7 允许 4 区间 3 派生 1 实际 76 多出 0 缺失 0 |nft accept 0 多出 0 |relay 口绑定回环=1/1 条`
|
|||
|
|
⇒ **10/12 → 11/12** 达成本单期望(原红项 `OBS-11` 由"77 ≠ 79"假红转为绿)。
|
|||
|
|
|
|||
|
|
**E3(真机受控临时口)** —— 口值 = **`127.0.0.1:27000`**(选址理由:**不在** required/allowed/ranges/derived/拨号池任何一处);起口方式 = `ssh 'timeout 45 bash -s' < tmplisten.sh`(**`timeout` 兜底 ⇒ 即使中断也会自动关闭**):
|
|||
|
|
|
|||
|
|
| 步 | 命令 | 实测 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 起口前 | `ssh bt-server "ss -lntp \| wc -l"` | **77** |
|
|||
|
|
| 起口后 | `ss -lntp \| wc -l` ⇒ **78**;跑探针 | `FAIL OBS-11 集合 … 多出 1 缺失 0`|**stderr 点名 `OBS-11 多出 127.0.0.1:27000`**|rc=**1** |
|
|||
|
|
| 关口后(`wait` 到 `timeout` 自然结束) | `ss -lntp \| wc -l` ⇒ **77**;再跑探针 | `PASS OBS-11 集合 … 多出 0 缺失 0`|rc=**1**(仅 OBS-09) |
|
|||
|
|
|
|||
|
|
⇒ **"多出必红、恢复必绿",且收口时该口已关闭、`ss` 回到 77(⛔ 零残留监听口)**。
|
|||
|
|
|
|||
|
|
### 8.6 nft 判据
|
|||
|
|
|
|||
|
|
- **`nft -j` 可用**(P2:`nftables v1.0.4`,首字符 `{`)⇒ **正路走 JSON**,未走退化。
|
|||
|
|
- 🔴 **归一后的 accept 集合 = ∅**。原因(本条同时纠正了一个旧口径):47 的 `nftables` 只有 **22 条规则**,而 **`ip filter INPUT` 链 0 条规则、`policy = accept`**;`nft list ruleset | wc -l = 72` 里绝大多数是 **`ip nat` / `dsh_egress`(output)** 与注释行 ⇒ **行数根本不是入站暴露面**(这正是"`NFT_RULES` 计数"这条旧判据的第二处结构缺陷,已在参数表内标注退役)。
|
|||
|
|
- 归一实现:取 **`hook=input` 的链 + 沿 `jump` 的传递闭包**,其内 verdict 含 `accept` 的规则 → `<proto>:<dport>`(`dport` 支持 数/区间/集合;无端口匹配 → `<proto>:any`)。
|
|||
|
|
- 判据 = `acceptSet ⊆ NFT_ALLOW_INBOUND`:今天 **`∅ ⊆ 白名单` 成立 ⇒ PASS**。
|
|||
|
|
- ⚠️ **诚实标注**:因为集合为 ∅,**当前白名单并未被"现实样本"压过** ⇒ 本棒为此补了**两条夹具**(`fx-nft-extra.json` 注入 `tcp:9999`;`fx-nft-text-extra.txt` 注入文本行)**证明"多出必红"**,⛔ 而不是让它"永远绿着过"。
|
|||
|
|
- **"说不清来源的入站 accept" = 0 条** ⇒ 无需转 §8.8。
|
|||
|
|
|
|||
|
|
### 8.7 零回归 / 越界自证
|
|||
|
|
|
|||
|
|
| 项 | 命令 | 实测 | 期望 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `npm test` | `npm.cmd test`(Node **v22.22.2**) | **# tests 160 / # pass 159 / # fail 0 / # cancelled 0 / # skipped 1**;rc=0 | ≥ 160/159/0/1 ✅(**与基线逐字一致 ⇒ 零回归**) |
|
|||
|
|
| `--scene all` | `node scripts/overlay-failover-drill.cjs --scene all` | **12 PASS / 0 SKIP / 0 FAIL**;rc=0(⚠️ 第 3 次尝试;见 §8.8-1) | 12 PASS / 0 FAIL ✅ |
|
|||
|
|
| 监听口 / nft | `ssh bt-server "ss -lntp \| wc -l; nft list ruleset \| wc -l"` | **77 / 72** | 77 / 72 ✅(**本单不产生新监听口**) |
|
|||
|
|
| 越界:`src/` | `git status --short src/ \| wc -l` | **20**(改动前 20 ⇒ **Δ0**) | Δ=0 ✅ |
|
|||
|
|
| 越界:总数 | `git status --short \| wc -l` | **43**(=改动前基线;⚠️ `scripts/overlay-probe.cjs` 是 **untracked**(`??`)⇒ **改它不改计数**) | 43 ✅ |
|
|||
|
|
| `.bak` 残留(仓内) | `git status --short \| grep -i bak` | **空** | 空 ✅(本单备份全在 `_中间产物_待清理/seq11/`,⛔ 不进仓) |
|
|||
|
|
| 演练 env 残留 | `ls /etc/systemd/system/dshs.service.d/` + `systemctl show dshs -p Environment` | 无 `zz-drill-override.conf`;`DRILL_*` 命中 **0** | 0 ✅ |
|
|||
|
|
| **E8 回滚可用** | `cp -a …bak-<ts> → probe` ⇒ 跑探针 ⇒ `cp -a …new-<ts> → probe` | 回滚后:`FAIL OBS-11 监听口=77(阈值 79) …` ⇒ **10/12**;恢复后 md5 = `d7e3ed77c1e0ea983b5ee879abde3130`(与改后一致) | 回到 10/12 ✅ |
|
|||
|
|
|
|||
|
|
### 8.8 未过项 / 在册未办(逐条:现象 / 已做到哪一步 / 回头条件)
|
|||
|
|
|
|||
|
|
**§8.8-1 🔴(新发现 · 环境级 · 非本单引入)106 的 `sshd` 被 MaxStartups 限流 ⇒ 脚本化 ssh 偶发 `rc=255`,会让整场演练中止**
|
|||
|
|
|
|||
|
|
- **现象**:`--scene all` 第 1 次在**幕 1 全绿后**崩(4 PASS / 0 FAIL,1m24s),第 2 次在**启动阶段**崩(8s,0 项),两次都停在 `overlay-failover-drill.cjs` 的 `ssh()` 抛 `Error: Command failed: ssh … test106 …`;**第 3 次通过(12 PASS)**。
|
|||
|
|
- **根因(证据级)**:106 的 `journalctl -u sshd --since '-25min'` 命中 **586** 条 error/refus/timeout/preauth(对照 47 仅 **3** 条),其中含原文 **`sshd[6366]: error: beginning MaxStartups throttling`**,另有大量 `Connection closed by authenticating user root … [preauth]`/`invalid user ubuntu`(**外部爆破流量**)+ `kex_exchange_identification: read: Connection reset by peer`。`/etc/ssh/sshd_config` 的 `MaxStartups` 为**默认 `10:30:100`**、106 的 **fail2ban `inactive`** ⇒ 未认证连接槽位被爆破流量打满 ⇒ **新连接被丢** ⇒ ssh `rc=255`。手工具复现:同一条重命令第 1 次 `rc=255`、第 2/3 次 `rc=0`(~700 ms)。
|
|||
|
|
- **已做到哪一步**:① 已确认**与本单改动无关** —— 本单零 `src/` 改动、且演练**不消费** `overlay-probe.cjs`,探针只连 47(47 侧 3 条错误、E1 全绿);② 已用**第 3 次尝试**取得 E6-2 的 12 PASS / 0 FAIL;③ 已定位到 sshd 侧的限流证据与爆破流量来源。
|
|||
|
|
- **回头条件**:① **给 106 的 sshd 调 `MaxStartups`/起 fail2ban/把 106 的运维 ssh 改走 47 跳板**(⚠️ 前两项属**放宽/收紧一个安全参数** ⇒ 其中"放开限流"命中 **R5**,**⛔ 本棒不做、不在本单范围内**);② 或让脚本化 ssh 具备**退避重试**(属改 `overlay-failover-drill.cjs` ⇒ 单外,**先报告**)。
|
|||
|
|
- **在册**:转入下一棒(**平台面既有缺陷**,会影响**任何**针对 106 的脚本化 ssh,不只是演练)。
|
|||
|
|
|
|||
|
|
**§8.8-2 🟡 与单不符的两处 —— 已按"实质判据"处理(供下一轮修单模板时吸收)**
|
|||
|
|
|
|||
|
|
1. **P1-注 的期望值不可满足(单方瑕疵)**:单内写 `grep -nE '\*' ss47-before.txt` ⇒ **零命中**。**实测不可满足** —— `ss -lntp` 的 **peer 列**恒为 `0.0.0.0:*`(76 行**每行都有** `*`)。⇒ 实质判据 = **Local Address:Port 列零 `*`**:`awk 'NR>1 && $1=="LISTEN"{print $4}' … | grep -c '\*'` = **0** ✅。⚠️ 顺带踩到一次:首跑 `ssh … > f 2>&1` 把 **3 行 ssh PQ 告警**写进了取证文件(77 → 80)⇒ **取证落盘一律 `2>/dev/null`**(已复取干净版)。
|
|||
|
|
2. **P4 / E7 的 `src/` 期望值有误(单方瑕疵)**:单内写「`src/` 改动 = **0**」。**实测 20**,且**与本单无关** —— 它是**既存未提交基线**(17 个 `M` + 3 个 `??`:`src/net/relay/`、`src/web/routes/overlay.ts`、`src/worker/relay-tunnel.ts`;后者正是 §5.9-**Q4** 的留档缺口)。⇒ 实质判据 = **本单不得新增 `src/` 改动** ⇒ 20 → **20(Δ0)** ✅。⚠️ 另一处易误读:总计数 43 **不因本单而变**,因为 `scripts/overlay-probe.cjs` 本身是 **untracked**(`??`)—— **不能拿"总数没变"当"没改文件"的证据**,判据要落到 `md5sum` 与 Δ 上。
|
|||
|
|
|
|||
|
|
**§8.8-3 📌 §5.9 的 Q1–Q5 原样转下一棒(⛔ 本棒未修,R7)**
|
|||
|
|
|
|||
|
|
| 序 | 缺陷 | 本棒动作 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **Q1** | `mksess*.cjs` 失效(`/opt/dshs/mksess*.cjs` 仍写 SQLite 旧库)⇒ **阻塞实例面验收** | ⛔ 未修,原样转办(**下一棒第一件**) |
|
|||
|
|
| **Q2** | guest(w-106) 实例页 **502** | ⛔ 未修,原样转办 |
|
|||
|
|
| **Q3** | `OBS-09` 实例面 `000`(**环境态非缺陷**) | ⛔ 未动(本棒**不起实例**;起实例属 Q3,与 Q2 同批最省) |
|
|||
|
|
| **Q4** | `src/net/relay/**` untracked 留档缺口 | ⛔ 只报告(**卡在用户授权**,不在技术侧) |
|
|||
|
|
| **Q5** | `client.ts.bak-seq7-*` 残留 | ⛔ 未动;本棒已**以身作则**立起"`.bak` 只在执行期存在"这条规矩(本棒仓内 `.bak` = 0) |
|
|||
|
|
| **Q6** | 参数表 `LISTEN_COUNT`=79 与现态的 1 条无名口 | ✅ **已结构化吸收**(集合判据不再依赖计数;那"1 条"就是 `20000` 实例档 ⇒ 现已在允许区间内)。⚠️ 剩余项「47 恢复活跃实例后复取应回 78 行」的复核**与 Q3 合批** |
|
|||
|
|
|
|||
|
|
**⚠️ 另记一条(本棒未触发但已备查)**:`RELAY_FAILOVER_COOLDOWN_MS` 全程 **未被触碰**;`grep` 演练 / 回滚 / 夹具路径中该键的 `=0` 形态 = **0 命中**(收口复核:`dshs.service.d` 下无 `zz-drill-override.conf`、`DRILL_*` env 命中 0)。
|
|||
|
|
|
|||
|
|
### 8.9 指纹(收口复取 · 全部现取)
|
|||
|
|
|
|||
|
|
| 项 | 命令 | 值 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **参数表**(§10 不计入) | `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` | **`8f08e74b026e6e5b5e1b3db813f031ae`**(出单基线 `99e9e17b0c1ce0550e4bc7626a5a0494` ⇒ **已变** ✅ E9) |
|
|||
|
|
| **本交接单前缀**(§8 及其后不计入) | `sed '/^## §8 回报格式/,$d' 交接单_观测口径与在册缺陷_20260917.md \| md5sum` | **`3ece0f870cba67d0113a4c5f9de9d812`**(**回填后未变** ✅) |
|
|||
|
|
| **交叉证据**:检测时延单(§8 前缀口径) | `sed '/^## §8 回报格式/,$d' 交接单_检测时延与deadline_20260917.md \| md5sum` | **`d903b4eeabf25ef381379cdbaac77e8a`**(未变 ✅) |
|
|||
|
|
| **探针(代码)** | `md5sum D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | 改前 `0cd76d986273e04631510aa7d215d16c` ⇒ **改后 `d7e3ed77c1e0ea983b5ee879abde3130`** |
|
|||
|
|
| **入口** | `md5sum 接续入口_覆盖网络线_20260916.md` | 收口取(本单未在此登记,避免自指) |
|
|||
|
|
| **原始取证落盘** | — | `_中间产物_待清理/seq11/`(P1 原文、6 份夹具、`nft47.json`/`nft-text.txt`、`status-fixture.json`、探针 `.bak-<ts>` 与 `.new-<ts>`、`npmtest.txt`、`drill-all-*.txt`、`real-*.txt`、生成器 `mk-fixtures.py`/`mk-nfttext-fx.py`/`nft-summary.py`)⇒ **保留供第三方复核**(⛔ 不写进正式档案) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §9 执行回报(序⑬ 执行棒 · 2026-09-17 15:2x–15:5x · 在册小缺陷清算 Q1→Q3)
|
|||
|
|
|
|||
|
|
### 9.1 结论(逐条)
|
|||
|
|
|
|||
|
|
| 序 | 结果 | 判据 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **Q1** | ✅ **已修并验收** | `/opt/dshs/mksess{,-guest}.cjs` 改插 **PG**;修前实证写进 SQLite(PG `sessions`=2 / SQLite=3);修后临时会话可直插 PG,**实例面 `curl` 回 `401`(⛔ 非 `000`)** |
|
|||
|
|
| **Q2** | 🔴 **未达成 —— 阻塞在一条新发现的数据面缺陷** | w-106 用户 `POST /api/dsh/enter` 仍 **500**;**真因不是单内记的 `landModels → writeHomeFile`**,而是「relay 流上的 HTTP keep-alive 复用」⇒ 见 **9.3** |
|
|||
|
|
| **Q3** | ✅ **达成(探针 12/12 · rc=0)** | `OBS-09` 绿(本机 `20000`=401、w-106 `42461`=401)**且**"有实例态"下 `OBS-11` 仍绿(实际 77 / 多出 0 / 缺失 0、`20000` 落在允许区间) |
|
|||
|
|
|
|||
|
|
### 9.2 Q1 · `mksess*.cjs` 从 SQLite 改到 PG(✅ 已修)
|
|||
|
|
|
|||
|
|
- **前提复现(D1)**:跑旧 `/opt/dshs/mksess-guest.cjs` ⇒ **PG `sessions` 仍是 2、SQLite `sessions` 变 3** ⇒ 会话落错库、平台读不到(R4 的实例面验收手段整体失效)。
|
|||
|
|
- **改法**:实现**只有一份** = `/opt/dshs/mksess.cjs` —— 连接串**从 `DSHS_DB_URL` env → `/etc/dshs.env` → `dshs.service.d/*.conf` 逐个找**,⛔ **不把凭据固化进 0644 脚本**;`mksess-guest.cjs` = `process.argv[2]='guest'; require('/opt/dshs/mksess.cjs')` 薄封装(保留原文件名 —— 档案 76/77 与 `dsh-plugin-diagnose` 等多处按此名引用)。
|
|||
|
|
- **验收**:`node mksess.cjs` ⇒ token 64 字符、PG `sessions` 2→3、`user_agent='poc-curl2'` 命中;**实例面 `curl -o /dev/null -w '%{http_code}' http://127.0.0.1:20000/` = `401`**(⛔ 非 `000`)。
|
|||
|
|
- **指纹**:`mksess.cjs` = **`a97c0f214650fbc552c07d804fd153be`**|`mksess-guest.cjs` = **`af6eab38c4ff5c365ee17e29d7b98a31`**。原件备份:`/opt/dshs/mksess{,-guest}.cjs.bak-seq13-20260917-152653`(本地副本 `_中间产物_待清理/seq13/`)。
|
|||
|
|
- **回滚**:`cp -a /opt/dshs/mksess*.bak-seq13-20260917-152653` 回原名 + `DELETE FROM sessions WHERE user_agent='poc-curl2'`。
|
|||
|
|
- ⚠️ **遗留(R7 · 只报告未动)**:只改了 `/opt/dshs/` 这一处;**文档库与技能里对 `mksess.cjs` 的描述("DB 直插")仍是旧口径** ⇒ 需要时另起一小棒同步。
|
|||
|
|
|
|||
|
|
### 9.3 Q2 · guest(w-106) `enter` 500 —— **真因是数据面(新缺陷)**
|
|||
|
|
|
|||
|
|
**现象(3/3 稳定复现)**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
POST /api/dsh/enter (guest) ⇒ 500
|
|||
|
|
{"statusCode":500,…,"message":"agent POST /fs/isdir → 400: {\"error\":\"Bad Request\",\"message\":\"Client Error\",\"statusCode\":400}"}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**最小复现(1 条命令,在 47 上跑)** —— 对**拨号池回环口**的**同一条 TCP 连接**连发两条请求:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
curl -s --http1.1 -w ' -> %{http_code}\n' -X POST -H 'content-type: application/json' \
|
|||
|
|
-H 'x-dsh-agent-token: <w-106 的 agent_token>' -d '{"userId":"<guest id>","relPath":""}' \
|
|||
|
|
http://127.0.0.1:25000/fs/isdir http://127.0.0.1:25000/fs/isdir
|
|||
|
|
⇒ {"isDirectory":true} -> 200
|
|||
|
|
⇒ {"error":"Bad Request","message":"Client Error","statusCode":400} -> 400
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(`/touch` 或 `/status` 打头、`/fs/isdir` 收尾同样复现;两条 GET 也复现 ⇒ **与请求体无关**。)
|
|||
|
|
|
|||
|
|
**取证链(逐层排除)**:
|
|||
|
|
|
|||
|
|
1. **客户端无辜**:Manager 侧 `strace -f -e trace=write,writev -p <dshs pid>` 抓到写出的就是一条**格式完全正确**的请求(`POST /fs/isdir HTTP/1.1`、`content-length: 62`、body 完整)。
|
|||
|
|
2. **走的就是拨号池口**:`ss` 见文件面连的是 `127.0.0.1:25000`;`[relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000`(`localPortFor('ops/w-106', 19000)`)。`/touch`、`/status` 因**并发**各占一条新连接 ⇒ **各占一条流 ⇒ 都成功**;`/fs/isdir` 落到**已被复用**的那条 ⇒ 失败。
|
|||
|
|
3. **400 的出处已定死**:body 与 Fastify `node_modules/fastify/fastify.js:985` 的 **`clientError` 分支**逐字一致 ⇒ 这是**收到非法字节流时写裸 socket 的兜底响应**,⛔ 不是任何业务路由返回的;它**不写 pino 日志** ⇒ 这正是"106 worker 日志里什么都没有"的原因。
|
|||
|
|
4. **106 侧抓包(`tcpdump -i lo -A -s0 'tcp port 19000'`)**:
|
|||
|
|
- 全场**只有 1 条** `POST /fs/isdir` 到达 agent,紧接着 agent 回 400;
|
|||
|
|
- 🔴 **关键**:`57244 > 19000` 的载荷是 **`HTTP/1.1 200 OK … {"isDirectory":true}`** —— 即**worker 侧把 agent 自己上一份响应回灌给了 agent** ⇒ agent 把响应行当请求行解析 ⇒ `clientError 400`。
|
|||
|
|
5. **因此**:责任面收敛到 **relay 的 stream 路由**(`src/net/relay/server.ts` 的 stream 表 / `workerStream` 与 `dialStream` 的映射)与 **worker 侧本地转发**(`client.ts#onOpenRequest` / `onRemoteData` → `st.tcp`)+ **拨号池收尾**(`dialer.ts` 的 `tcp.pipe(duplex).pipe(tcp)`,`duplex` 关闭时**没有** `tcp.destroy()`)。
|
|||
|
|
|
|||
|
|
**影响面(⚠️ 比在册描述严重)**:不是"guest 一个用户的 502",而是 **任何 `via='relay'` 的 host(今天 = w-106)** —— 只要一次业务动作对同一 host 发**第 2 条** agent 请求(`enter` 的 `isDirectory` → `launch` → `waitUpOnStatus` 轮询…)就必失败 ⇒ **w-106 用户"登录直达工作区"整体不可用(用户可见)**。
|
|||
|
|
|
|||
|
|
**已做到哪一步 / 卡在哪**:故障面已收敛到上面那一组文件,**但帧级取证(relay 侧 `workerStream`/`dialStream` 路由表逐帧日志)未做** ⇒ 尚未定位到具体代码行。
|
|||
|
|
|
|||
|
|
**回头条件(必须回头)**:① 拿到 relay 帧级路由证据并定位代码行;② 修复判据 = guest `enter` 回 **200**、同一连接连发两次都 **200**、`npm test` 不退化(基线 160/159/0/1)、`--scene all` 不退化、`overlay-probe` 保持 **12/12**。
|
|||
|
|
|
|||
|
|
**⛔ 本棒未动手(R7 + R11)**:这是**数据面**变更;在拿到帧级证据前盲改(例如只给拨号池补 `tcp.destroy()`)会把"静默给出错误答案"换成别的形态,还可能**掩盖 relay 侧的真错** ⇒ 属净风险,留作下一棒主题(**序 ⑭**)。
|
|||
|
|
|
|||
|
|
### 9.4 Q3 · `OBS-09` +「有实例态下的 `OBS-11`」(✅ 达成 · 探针 **12/12**)
|
|||
|
|
|
|||
|
|
- **起实例**:① **admin(w-47,本机直连,不经 relay)** 经 `POST /api/dsh/enter` ⇒ **`200`**、`port=20000`、`status=running`(这同时就是 **Q1 的实例面验收**:`curl http://127.0.0.1:20000/` = **401**,⛔ 非 `000`);② **guest(w-106)** 因 9.3 的缺陷堵在 `enter`,改由 **106 的 worker agent `POST /launch`**(带 `uid=100002` + `ws` 目录)拉起 ⇒ `200`、`port=21000`、`pid=237067`。⚠️ **该实例未经 Manager 租约**(为把 `OBS-09` 的对端腿测出来而"出格"了一步,已在工作区日志登记)。
|
|||
|
|
- **探针结果**:**`12/12 PASS · rc=0`**,其中
|
|||
|
|
- `PASS OBS-09 实例面 本机:20000=401 w-106:42461=401 (阈值 ∈ {200,401})`
|
|||
|
|
- `PASS OBS-11 集合 必在 7 允许 4 区间 3 派生 2 实际 77 多出 0 缺失 0 |nft accept 0 多出 0 |relay 口绑定回环=1/1 条` —— **正是 Q3 要的那一问:有实例态下 `20000` 落在允许区间**
|
|||
|
|
- `PASS OBS-08 端点表 2 条 / 离线 0 条`(比无实例态多 1 条)
|
|||
|
|
- `ss -lntp | wc -l` = **78 = 1 表头 + 77 socket**(无实例态 76 + 实例 `20000` 一口)⇒ **无多余监听**。
|
|||
|
|
- **回滚(要回到"无实例态")**:`systemctl stop dsh-100002-*.scope`(106)+ 停 w-47 那个实例;此后 `OBS-09` 回到红 —— 那是**环境态,非缺陷**。
|
|||
|
|
- ⚠️ **回头条件**:guest 实例是**出格拉起**的;一旦 Manager 的 reaper 回收它 ⇒ `OBS-09` 对端腿回 `000`(**环境态**,按 Q3 语义非缺陷)。
|
|||
|
|
|
|||
|
|
### 9.5 其余(R7 · 只报告,⛔ 未动手)
|
|||
|
|
|
|||
|
|
| 序 | 状态 |
|
|||
|
|
|---|---|
|
|||
|
|
| **Q4** | `src/net/relay/**` untracked 留档缺口 —— **未动**(卡在提交授权;`git status --short src/net/relay/` = 1) |
|
|||
|
|
| **Q5** | `src/net/relay/*.bak-seq7-*` 残留 —— **未动**(本棒仓内 `.bak` = 0,规矩继续) |
|
|||
|
|
| **Q6** | 已被序⑫ 结构化吸收;本棒复取 `实际 77`、`20000` 在允许区间 ⇒ **可关闭** |
|
|||
|
|
| 🔴 新发现 | **9.3 的 relay keep-alive 缺陷**(= 下一棒主题) |
|
|||
|
|
|
|||
|
|
### 9.6 越界自证 & 三条硬门
|
|||
|
|
|
|||
|
|
- `git status --short` = **43**(Δ **0**)|`git status --short src/` = **20**(Δ **0**)|仓内 `.bak` = **0**。
|
|||
|
|
- **D1**:全程**未改任何生产值** —— 参数表指纹仍 `8f08e74b026e6e5b5e1b3db813f031ae`、`RELAY_FAILOVER_*` / `HB_SEC` / burst 一字未动。
|
|||
|
|
- **R7**:Q4 / Q5 只报告;文档库・技能侧对 `mksess` 的旧描述未改(见 9.2 遗留)。
|
|||
|
|
- 🔴 `RELAY_FAILOVER_COOLDOWN_MS=0`:**0 命中**(未写入任何回滚 / 演练 / 夹具路径)。
|
|||
|
|
- **R4 收尾**:临时会话用完即删(`DELETE … user_agent='poc-curl2'` ⇒ `DELETE 2`,`sessions` 回 **2 = 基线**)。
|
|||
|
|
|
|||
|
|
### 9.7 指纹(收口现取)
|
|||
|
|
|
|||
|
|
| 项 | 值 |
|
|||
|
|
|---|---|
|
|||
|
|
| Q1 产物(47) | `mksess.cjs` **`a97c0f214650fbc552c07d804fd153be`**|`mksess-guest.cjs` **`af6eab38c4ff5c365ee17e29d7b98a31`** |
|
|||
|
|
| 参数表(§10 不计入) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证) |
|
|||
|
|
| 本交接单 §8 前缀(追加 §9 后复取) | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— `§9` 在 §8 之后,不进前缀口径) |
|
|||
|
|
| 探针(代码,未改) | `d7e3ed77c1e0ea983b5ee879abde3130` |
|
|||
|
|
| 原始取证落盘 | `_中间产物_待清理/seq13/`(`mksess.cjs` / `mksess-guest.cjs` / `probe-isdir.cjs` / `q2-capture.sh`);106 抓包与 47 strace 原文留在各机 `/tmp`(`cap.pcap` / `st2.txt`) |
|
|||
|
|
| 下一棒 | automation **`3e0a7b01-9257-4b76-b82f-4998c9f7eab0`**(`scheduledAt` = 2026-09-17 **15:53**,`nextRunAt` = 1789631580000) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §10 执行回报(序⑭ 执行棒 · 2026-09-17 15:53–16:1x · relay 流 keep-alive 复用缺陷)
|
|||
|
|
|
|||
|
|
### 10.1 结论(逐条)
|
|||
|
|
|
|||
|
|
| 项 | 结果 |
|
|||
|
|
|---|---|
|
|||
|
|
| **D1 · 先复现** | ✅ 复现,与 §9.3 逐字一致:同一条连接连发两条 ⇒ `{"isDirectory":true} -> 200` + `{"error":"Bad Request","message":"Client Error","statusCode":400} -> 400` |
|
|||
|
|
| **② 帧级取证 → 定位到行** | ✅ **定位到 `src/net/relay/client.ts#openStream` 里的一行 `duplex.on('data', …)`**(原 975 行) |
|
|||
|
|
| **③ 先红后绿** | ✅ 红:目标端收到 **2 段**回灌字节 `HTTP/1.1 200 OK …`;绿:修后 `T23` 通过(无杂音) |
|
|||
|
|
| **④ 端到端验收** | ✅ guest `enter` **200**、同连接连发两次 **200/200**(三次 **200/200/200**);`npm test` **160 pass / 0 fail / 1 skip**(基线 159 pass +T23);`--scene all` **12 PASS / 0 SKIP / 0 FAIL**;`overlay-probe` **12/12 PASS · rc=0** |
|
|||
|
|
|
|||
|
|
### 10.2 复现(D1 自证)
|
|||
|
|
|
|||
|
|
命令 = §9.3 原文那一条(47 上,对拨号池口同连接连发两条)。**修前**:`200` 然后 `400`。⛔ 全程未改任何生产值。
|
|||
|
|
|
|||
|
|
### 10.3 帧级取证与定位(**这是本棒的核心**)
|
|||
|
|
|
|||
|
|
**真因**:`RelayClient#openStream()` 把**读侧**接回了**出向**。
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/net/relay/client.ts(修前,openStream 内)
|
|||
|
|
const duplex = new MuxDuplex({ onOut: (chunk) => this.pumpDial(id, chunk), … })
|
|||
|
|
this.dialStreams.set(id, st)
|
|||
|
|
duplex.on('data', (chunk: Buffer) => this.pumpDial(id, chunk)) // 🔴 就是这一行
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**机制(逐跳)**:目标(agent)的响应 → worker `tcp.on('data')` → `pumpToRelay` → relay → 拨号方 `onRemoteData` → `dial.duplex.feed(payload)` → `push(chunk)` 触发 `'data'` → 🔴 **该监听把它 `pumpDial()` 又打回 relay** → worker 写进 agent socket ⇒ agent 拿 `HTTP/1.1 200 OK` 当**请求行**解析 ⇒ 非法字节流 ⇒ Fastify `clientError` 兜底回 `400`(写裸 socket、**不写 pino 日志**)。
|
|||
|
|
|
|||
|
|
⇒ **`200` 被回灌 ⇒ agent 回 `400` ⇒ 该 `400` 顺着同一条 keep-alive 连接被 curl 当成"第 2 条请求的响应"** —— 这就是"第 2 条必 400"的真实来源(curl 打印的 `400` **不是**第 2 条请求的应答,而是第 1 条响应的回灌回声)。
|
|||
|
|
|
|||
|
|
**为什么现有测试抓不到**:`test/relay.test.mjs#dialRoundTrip` 一凑够 `text.length` 就 `resolve`,多出来的那份回声**没人看**。
|
|||
|
|
|
|||
|
|
**排除项(逐层)**:relay 服务端无辜 —— `server.ts#onData → onDialerData` 只把 DATA 投给**对端**(`st.peer`),不镜像回发送方;错误方向只能是拨号方自己产生的。`dialer.ts` 的 `tcp.pipe(duplex).pipe(tcp)` 形状正确(`pipe` 走 `_write` → `onOut`)。
|
|||
|
|
|
|||
|
|
### 10.4 改法(唯一改动)
|
|||
|
|
|
|||
|
|
- **`src/net/relay/client.ts`**:**删掉**那一行 `duplex.on('data', …)`,原地换成一段⛔禁挂说明(含 106 抓包证据与 T23 指针)。
|
|||
|
|
- **为什么删是安全的**:出向唯一入口是 `_write` → `onOut` → `pumpDial`;`duplex.write()` 与 `tcp.pipe(duplex)` **都走 `_write`** ⇒ 两者本就是同一条路,那个监听**纯属多余且方向错**。
|
|||
|
|
- **附带修好的一条**:读侧不再被提前"吃掉" ⇒ `openStream()` 返回到 `tcp.pipe(duplex)` 之间到达的响应会**留在读缓冲**而不是被丢/被回灌。
|
|||
|
|
- **`test/relay.test.mjs`**:新增 **T23**「拨号流严格单向」—— 目标端**逐帧记账**(`POST …` 记 `requests`、其余记 `garbage`),一旦入向回灌 ⇒ `garbage` 非空 ⇒ **断言点名**(不是"计数差不多"那种软判据)。
|
|||
|
|
- 文件已在 `package.json` 的 `npm test` 列表内 ⇒ ⛔ **未改 `package.json`**(守住"改动不超出 `src/net/relay/**` + 其测试")。
|
|||
|
|
|
|||
|
|
### 10.5 先红后绿(原文级)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
修前(旧 lib):
|
|||
|
|
not ok 1 - T23 拨号流严格单向:入向的响应 ⛔ 不得被回灌进 agent socket
|
|||
|
|
拨号流不是单向的 —— 入向字节被回灌进 agent socket(共 2 段):
|
|||
|
|
["HTTP/1.1 200 OK\r\ncontent-length: 19\r\n\r\n{\"isDirectory\":true}", …(同上)]
|
|||
|
|
|
|||
|
|
build 后:ok 1 - T23 … # pass 1 / # fail 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
⚠️ **过程中踩到并修掉的测试卫生坑**:`onDown` → `teardownDialStreams()` 会 `duplex.destroy(new Error('link down: …'))`,而 `MuxDuplex` 上没有 `'error'` 监听 ⇒ Node 抛 **uncaughtException**,表现 =「T23 自己 pass、整个文件 fail」。生产侧 `dialer.ts` 有同形监听,测试里补上即可(已在 T23 的 `t.after` 注释里写死原因)。
|
|||
|
|
|
|||
|
|
### 10.6 端到端验收(原文级)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
① guest POST /api/dsh/enter(同连接 ×2)
|
|||
|
|
{"kind":"session","instance":{"id":"ae024d2c-…","port":21000,"status":"running"},"url":"https://guest.alotbuy.com/?token=…"} -> 200
|
|||
|
|
(同上)-> 200
|
|||
|
|
|
|||
|
|
② 帧级复验:已分配的池口 = 25000(同连接 ×3)
|
|||
|
|
{"isDirectory":true} -> 200 / 200 / 200 (修前 = 200 后必 400)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- `npm test` = **160 pass / 0 fail / 1 skip**(= 基线 159 pass + T23,**零退化**)
|
|||
|
|
- `--scene all` = **12 PASS / 0 SKIP / 0 FAIL**(幕1-A 17874ms、幕4-A 18754ms 均 < deadline 30000ms)
|
|||
|
|
- `overlay-probe`(带参数表)= **12/12 PASS · rc=0**
|
|||
|
|
- **部署**:`client.js`(`b8b29afba06ed6347cbefd29091f8c73`)铺到 **5 处** —— 47 `/opt/dshs/lib`、47 `/opt/dshs-cluster/lib`、47 `/opt/dsh-relay/lib`、106 `/opt/dshs-cluster/lib`、106 `/opt/dsh-relay/lib`;各留 `.bak-20260917-1558xx`。
|
|||
|
|
- **只重启了 47 的 `dshs`(Manager = 拨号方)**;`dshs-relay` / 106 `dshs-worker` **未重启**(改动是**拨号方专属**一行,worker/relay 侧行为逐字未变)。
|
|||
|
|
- **R7 传播面自证**:铺之前对本机 `lib/net/relay/*.js` 与 47 `/opt/dshs/lib/net/relay/*.js` 逐文件对账 ⇒ **Δ 只有 `client.js` 一个文件**(无夹带改动)。
|
|||
|
|
|
|||
|
|
### 10.7 越界自证 & 三条硬门
|
|||
|
|
|
|||
|
|
- **D1**:参数表指纹 **`8f08e74b026e6e5b5e1b3db813f031ae`(未变)** ⇒ **未改任何生产值**;`RELAY_FAILOVER_*` / `HB_SEC` / burst 一字未动。
|
|||
|
|
- **R7**:`git status --short` = **43**、`src/` = **20**(均 Δ0);仓内 `.bak`/`.tgz` = **0**;⛔ **未 commit / 未 push**。
|
|||
|
|
- 🔴 `RELAY_FAILOVER_COOLDOWN_MS` 置 0:**本机仓 0 命中 / 47 lib 0 命中**(未写进任何回滚·演练·夹具路径)。
|
|||
|
|
- **R4**:临时会话用完即删(`DELETE 1` ⇒ `sessions` 回 **2 = 基线**)。
|
|||
|
|
- **未超范围**:改动只有 `src/net/relay/client.ts` + `test/relay.test.mjs` 两个文件,**未动 `package.json`、未动 worker 侧协议、未重启 relay 之外的服务**(Manager 重启属本次修复必需,已按 R8 直接做)。
|
|||
|
|
|
|||
|
|
### 10.8 🔴 新发现在册缺陷 B(R7 · **只报告,未动手**)
|
|||
|
|
|
|||
|
|
| 项 | 内容 |
|
|||
|
|
|---|---|
|
|||
|
|
| **现象** | 拨号池**未分配**的槽位,被**任意一条连接**碰到后 ⇒ **该槽位永久死亡**;而 `localPortFor()` 之后照样把这个口号发出去 ⇒ **`ECONNREFUSED`**(本棒实测:`[relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000` 紧接 `connect ECONNREFUSED 127.0.0.1:25000`,`enter` 回 500 `fetch failed`) |
|
|||
|
|
| **出处** | `src/net/relay/dialer.ts#onConn`:`const key = slot.key; if (key === undefined) { slot.server.close(); tcp.destroy(); return }` —— 关闭**服务器本身**,却**不把该槽位从 `slots` 里摘掉**,于是 `key` 仍是 `undefined` ⇒ `localPortFor()` 的 `slots.find(s => s.key === undefined)` **下次还会选中它** |
|
|||
|
|
| **怎么被触发** | 本棒**自己**触发:口池刚绑好、落点还没分配时,对 `127.0.0.1:25000` 打了一次探针(§9.3 那条命令的"提前版")⇒ 25000 当场死掉;池报"64 个口"而 `ss` 只见 **63**,正是这个缺口 |
|
|||
|
|
| **为什么算缺陷** | 属本线反复强调的"**静默给出错误答案**"同族:**失败被推迟**到下一次分配,且现场只有 `ECONNREFUSED`,看不出"这个槽位早废了" |
|
|||
|
|
| **候选修法(⛔ 未实施,留给下一棒先取证)** | ① `onConn` 撞到未分配槽位时**只 destroy 连接、不 close 服务器**(最小改动,但"外部随机连入"仍会被静默丢弃);② close 之后**把槽位从 `slots` 摘掉/标记废弃**,让 `localPortFor` 不再选中(更彻底,需处理 `bound` 计数与日志);③ 池口号改绑在**独立的不可路由网段/更冷门口位**并加"首次使用才分配"的显式状态机。**判据**:未分配槽位被连接后,`status().pool` 与实际在听口号数必须**恒等**,且 `localPortFor` 永不返回已死口号 |
|
|||
|
|
| **回头条件** | 单独一棒处理;⛔ 在拿到"槽位被谁连、何时连"的帧级证据前**不要**只改 `onConn` |
|
|||
|
|
|
|||
|
|
### 10.9 其余在册未办(原样转下一棒,⛔ 未动手)
|
|||
|
|
|
|||
|
|
| 序 | 状态 |
|
|||
|
|
|---|---|
|
|||
|
|
| **Q4** | `src/net/relay/**` **untracked** 留档缺口 —— ⚠️ **本棒的修复正好落在该缺口内**(`client.ts` 是新文件、`test/relay.test.mjs` 也是 `??`)⇒ 二者目前**只存在于工作区,不在版本库里**。commit / push 仍需**你的明确授权**(本棒未获授权,未动)。 |
|
|||
|
|
| **Q5** | `src/net/relay/*.bak-seq7-*` 残留 —— 未动(本棒仓内 `.bak` = 0) |
|
|||
|
|
| **seq13 遗留** | 文档库・技能侧对 `mksess.cjs` 的描述("DB 直插")仍是旧口径 ⇒ 需同步时另起一小棒 |
|
|||
|
|
| **长期在册** | `dsh-auto-handoff-chain` 在文档库**无副本** ⇒ 该技能的"三处同步"从未建立 |
|
|||
|
|
| **§8.8-2** | 幕4-A 耗时仍**贴近** deadline(本次 18754ms / 30000ms)⇒ 临界项,未恶化也未解决 |
|
|||
|
|
|
|||
|
|
### 10.10 指纹(收口现取)
|
|||
|
|
|
|||
|
|
| 项 | 值 |
|
|||
|
|
|---|---|
|
|||
|
|
| **修复源文件** | `src/net/relay/client.ts` = **`6ffb117f3f6df1c2456641e176529ce8`** |
|
|||
|
|
| **测试文件** | `test/relay.test.mjs` = **`14aa2808bc6446d34af19306d43c4e8d`**(1149 行,含 T23) |
|
|||
|
|
| **部署产物** | `lib/net/relay/client.js` = **`b8b29afba06ed6347cbefd29091f8c73`**(五处远端同值) |
|
|||
|
|
| 参数表(§10 不计入) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证) |
|
|||
|
|
| 本交接单 §8 前缀(追加 §10 后复取) | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— §10 在 §8 之后,不进前缀口径) |
|
|||
|
|
| 原始取证落盘 | `_中间产物_待清理/seq14/`(`deploy-seq14.sh` / `evidence-47.sh` / `final-verify.sh`);`--scene all` 原文 = 本地 `/tmp/seq14-scene-all.txt` |
|
|||
|
|
| 下一棒 | automation **`1933b18a-0c2a-46a2-829c-440a900fed08`**(「覆盖网络线-序15执行棒-拨号池槽位自毁缺陷B修复」,一次性,`scheduledAt` = 2026-09-17 **16:14**,`nextRunAt` = 1789632840000) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §11 执行回报(序⑮ 执行棒 · 2026-09-17 16:14–16:3x · 拨号池未分配槽位自毁「缺陷 B」)
|
|||
|
|
|
|||
|
|
### 11.1 结论(逐条)
|
|||
|
|
|
|||
|
|
✅ **缺陷 B 已修并端到端验收**:`src/net/relay/dialer.ts`(1 个源文件)+ `test/relay.test.mjs`(新增 T24);判据 **a / b / c 三条全绿**;⛔ 未改任何生产值、⛔ 未 commit / push、**零上抛**;收口 6 件全办。
|
|||
|
|
|
|||
|
|
### 11.2 复现(D1:先复现、后动手 —— ⛔ 没在"只看到 `ECONNREFUSED`"的情况下动 `onConn`)
|
|||
|
|
|
|||
|
|
**① 进程内确定性复现**(`_中间产物_待清理/seq15/repro-defect-b.mjs`;私有口段 47000、私有实例段 47100,**零生产影响**)—— 修前原文:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
① 池就绪(注册=true):status().pool = 4 · 实际在听 = 4 · 区间 47000..47003
|
|||
|
|
② 对未分配口 47000 发一条连接 ⇒ connected
|
|||
|
|
status().pool = 4 · 实际在听 = 3 ⇒ 🔴 判据 a 不等(池在撒谎)
|
|||
|
|
该路径日志行数 = 0
|
|||
|
|
③ localPortFor(ops/w-xs, 47110) = 47000(死口 = 47000)
|
|||
|
|
④ 🔴 判据 b:落点不通 = ECONNREFUSED
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**② 47 现网事实**(只读取证,落盘 `A47.txt`;⛔ 全程未连任何池口):
|
|||
|
|
|
|||
|
|
- `dshs` 于 **16:09:11** 重启 ⇒ 池口 **64 个全在听**(`ss -lntH 'sport >= :25000 and sport <= :25063' | wc -l` = 64)、`ss -lntp` 总行数 **79**、逐口核对 **无缺失**。
|
|||
|
|
- journal 里 **`ECONNREFUSED 127.0.0.1:25000` 全量 3 条,全部落在 16:00:31 / 16:00:35 / 16:00:40**,全部是 `POST /api/dsh/enter` → **500 `fetch failed`**(栈 = `supervisor/remote-spawner.js:142` ← `:211` ← `web/routes/dsh.js:200`)。
|
|||
|
|
- 而这三条**紧跟在 16:00:27 的 `[relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000` 之后** ⇒ **"落点已分配、却连不上"** —— 这正是"该槽位早就死了"的**演绎证据**(唯一能关掉池槽位监听又保留槽位的路径就是 `onConn` 的 `key === undefined` 分支)。
|
|||
|
|
- **谁 / 何时连了未分配槽位**:**是我们自己的取证探针触发的,不是自然发生** —— 该路径**零日志零计数**(全量 journal `grep -c '未分配'` = **0**),只能靠上面那组矛盾反推。⇒ 这也正是"必须补日志/计数"的直接理由。
|
|||
|
|
|
|||
|
|
### 11.3 定位(到代码行)
|
|||
|
|
|
|||
|
|
`src/net/relay/dialer.ts#onConn`:
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
const key = slot.key
|
|||
|
|
if (key === undefined) { slot.server.close(); tcp.destroy(); return }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
关掉的只是**这个口的服务器**,而槽位**仍留在 `slots` 里、`key` 仍是 `undefined`** ⇒ `localPortFor()` 的 `slots.find((s) => s.key === undefined)` **下次还会选中它** ⇒ 把一个没人听的口号当落点发出去。**失败被推迟到下一次分配**,现场只有 `ECONNREFUSED`。
|
|||
|
|
|
|||
|
|
### 11.4 改法(三个候选里取 ①,理由 = R11)
|
|||
|
|
|
|||
|
|
1. **`onConn`:⛔ 不再 `close()` 服务器,只 `tcp.destroy()` + 计数(新增 `stray`)+ 点名日志。**
|
|||
|
|
⛔ **不取候选 ②**(把槽位从 `slots` 摘掉):那等于让**任意一条本地连接**都能**永久**蚕食池容量(扫 64 次即可把池扫空)= **净退化(R11)**;候选 ③(换口段 + 显式状态机)超出 `src/net/relay/**` 必要范围。
|
|||
|
|
2. **纵深防御**:`localPortFor()` 的空槽查找与 `lruIdle()` 都**只认 `server.listening` 的槽**;一个可成交槽都没有时**点名"哪几个口已不在听"**再拒(失败关闭,⛔ 绝不发死口号)。
|
|||
|
|
3. **`status()`**:`pool` 改报**实际还在听的槽位数**(不再报 `start()` 那一刻的常量)+ 新增 `stray` 计数 —— 让"池在撒谎"这类事**不可能不被看见**。
|
|||
|
|
|
|||
|
|
### 11.5 先红后绿(原文级)
|
|||
|
|
|
|||
|
|
- **红**(新用例 + 旧 `lib/`):`判据 a:池账(3) 与实际在听(2) 必须恒等 —— 不等即"池在撒谎"` ⇒ `3 !== 2`(`ERR_ASSERTION`)。
|
|||
|
|
- **绿**(`npm run build` 后):`ok 1 - T24 拨号池:未分配槽位被一条连接命中后 ⛔ 不得自毁…` / `# pass 1 / # fail 0`。
|
|||
|
|
- 新用例 = **T24**(`test/relay.test.mjs` 尾部,文件 1318 行);⛔ 未改 `package.json`。
|
|||
|
|
|
|||
|
|
### 11.6 端到端验收(47 现网原文)
|
|||
|
|
|
|||
|
|
**① 收口终验**(`verify-seq15.sh`,含一次 `systemctl restart dshs`)—— ⚠️ 这个顺序**就是缺陷 B 的复现顺序**:
|
|||
|
|
|
|||
|
|
| 步 | 原文 |
|
|||
|
|
|---|---|
|
|||
|
|
| 干净池基线 | 池口在听数 = **64** |`[relay-dialer] 本机落点池就绪:64 个口(25000..26000)` |
|
|||
|
|
| 🔴 **故意对未分配落点 25000 发一条连接** | `TCP 已连上` ⇒ **打后池口在听数仍 64**、stray 日志 **1** 行:`[relay-dialer] ⛔ 未分配落点 127.0.0.1:25000 收到一条连接 ⇒ 只丢弃该连接、槽位保留(累计 1 次)` |
|
|||
|
|
| guest `POST /api/dsh/enter`(同连接 ×2) | **200 / 200**(`instance.port=21000 status=running`) |
|
|||
|
|
| 帧级(该落点口) | 落点口 = **25000**(就是刚被打过的那一个)|该口在听 = 1|同连接连发 3 次 ⇒ **200 / 200 / 200**(`{"isDirectory":true}`) |
|
|||
|
|
| R4 | `sessions` 清理后回 **2**|结束态池口 = 64 |
|
|||
|
|
|
|||
|
|
**② 最终态复验**(`verify-seq15-final.sh`,⛔ **不重启**,就在收口现场把同一顺序再走一遍)= `verify-47-final.txt`,逐项与 ① 同(64 → 触发 → **仍 64 / stray=1** → **200 / 200** → 25000 在听 200×3 → 结束态 64)。
|
|||
|
|
|
|||
|
|
**③ 零回归**:`npm test` = **162 tests / 161 pass / 0 fail / 1 skip**(基线 160 pass + 新 T24)|`--scene all` = **12 PASS / 0 SKIP / 0 FAIL**(幕 4-A 19537 ms、幕 4-C 20809 ms / 30000 ms)|`overlay-probe --table "E:/…/参数表_覆盖网络_20260917.md"` = **12/12 PASS · rc=0**(`OBS-11 集合 … 实际 78 多出 0 缺失 0`、`OBS-09 本机:20000=401 w-106:33909=401`)。
|
|||
|
|
|
|||
|
|
**部署**:`lib/net/relay/dialer.js` = **`6446fe9b23bca2649137adcbf4bf9d1f`**,**五处同值**(47:`/opt/dshs` `/opt/dshs-cluster` `/opt/dsh-relay`;106:`/opt/dshs-cluster` `/opt/dsh-relay`,106 无 `/opt/dshs`);每处各留 `.bak-20260917-1621xx`;**只重启了 47 的 `dshs`**(拨号方专属改动)。
|
|||
|
|
|
|||
|
|
### 11.7 越界自证 & 三条硬门
|
|||
|
|
|
|||
|
|
| 门 | 自证 |
|
|||
|
|
|---|---|
|
|||
|
|
| **D1** | 参数表指纹(§10 截断口径)仍 **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变**)|⛔ 未动 `RELAY_FAILOVER_*` / `HB_SEC` / burst |
|
|||
|
|
| **R7** | `git status` 总行数 **43**、`src/` **20** 均为既存基线(Δ0);本棒只改 `src/net/relay/dialer.ts` + `test/relay.test.mjs`(两者同处 untracked 的 `src/net/relay/**`、`test/` 属**既有缺口**)|⛔ 未 commit / push|Q4 / Q5 **只报告未动手**;范围**未越出** `src/net/relay/**` + 其测试(⛔ 未动 worker 侧协议、未重启 relay / 106) |
|
|||
|
|
| **COOLDOWN** | 远端**严格判据(赋 0)0 命中**|⚠️ 本棒 `verify-seq15.sh` 里的宽松正则 `"\?0"\?` 会**误报 2** —— 命中 `switcher.js:53` 的默认字面量 `num('RELAY_FAILOVER_COOLDOWN_MS', 300_000)` 里的 `0`;口径以「值被赋 0」为准(`grep -E '=\s*0\|, 0\)\|"0"'`) |
|
|||
|
|
|
|||
|
|
### 11.8 其余在册未办(原样转下一棒,⛔ 未动手)
|
|||
|
|
|
|||
|
|
| 序 | 状态 |
|
|||
|
|
|---|---|
|
|||
|
|
| **Q4** | `src/net/relay/**` untracked 留档缺口 —— ⛔ **卡在"提交 / 推送授权"**(本棒未获授权,未动;本棒的修复与 T24 同样只存在于工作区) |
|
|||
|
|
| **Q5** | 代码仓 `src/net/relay/*.bak-*` 残留 **3 个**(本棒未清,R7) |
|
|||
|
|
| **seq13 遗留** | 文档库 / 技能侧对 `/opt/dshs/mksess.cjs` 的「DB 直插」描述仍是旧口径(实际已改 PG) |
|
|||
|
|
| **长期在册** | 技能 `dsh-auto-handoff-chain` 在文档库**无副本** ⇒ 该技能「三处同步」从未建立 |
|
|||
|
|
| **§8.8-2** | 幕 4-A 耗时 **19537 / 20809 ms**(贴近 deadline 30000 ms)⇒ 临界项,未恶化也未解决 |
|
|||
|
|
|
|||
|
|
### 11.9 指纹(收口现取)
|
|||
|
|
|
|||
|
|
| 项 | 值 |
|
|||
|
|
|---|---|
|
|||
|
|
| **修复源文件** | `src/net/relay/dialer.ts` = **`f3a608a75f7edd0aa0de8cceb4de5615`** |
|
|||
|
|
| **测试文件** | `test/relay.test.mjs` = **`587f9d80e9330e33badb281e6bd6c719`**(1318 行,含 T24) |
|
|||
|
|
| **部署产物** | `lib/net/relay/dialer.js` = **`6446fe9b23bca2649137adcbf4bf9d1f`**(五处远端同值) |
|
|||
|
|
| 参数表(不计入 §11 口径) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证;口径 = `sed '/^## §10 指纹/,$d' … \| md5sum`) |
|
|||
|
|
| 本交接单 §8 前缀(追加 §11 后复取) | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— §11 在 §8 之后,不进前缀口径) |
|
|||
|
|
| 原始取证落盘 | `_中间产物_待清理/seq15/`:`evidence-A-47.sh`+`A47.txt`|`repro-defect-b.mjs`|`deploy-seq15.sh`+`deploy-47.txt`/`deploy-106.txt`|`verify-seq15.sh`+`verify-47.txt`|`verify-seq15-final.sh`+`verify-47-final.txt`|`npmtest-seq15.txt`|`scene-all.txt`|`probe.txt` |
|
|||
|
|
| **下一棒** | automation **`91c53ef8-c3f0-4bb3-9f0a-943790178176`**(「覆盖网络线-序16规划棒-会合中继拆分复核与在册收尾定序」,一次性,`scheduledAt` = 2026-09-17 **16:36**,`nextRunAt` = 1789634160000) |
|
|||
|
|
|
|||
|
|
### 11.10 范围外 · 只报告(R7,⛔ 未动手)
|
|||
|
|
|
|||
|
|
**ssh 别名 `bt-server` 指向的 `32022` 是失效残留口。** 本机 `~/.ssh/config` 里 `bt-server` 写的是 `Port 32022`,而 47 上 **sshd 只监听 `22`**(`/etc/ssh/sshd_config:151 Port 22`;master pid 733418 **启动于 2026-09-16 22:12:48**、`/etc/ssh/` 下**无** `sshd_config.d` drop-in;`32022` 无监听)。项目脚本**一律显式写 `ssh -p 22 bt-server`**(近 20 份脚本 42 处均如此)⇒ 只有"直接敲别名"会 `Connection refused`。本棒为绕开它**多花了 3 次调用**;⛔ 未改 47 的 sshd_config、⛔ 未改本机 ssh config(等指示)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §12 执行回报(序⑯ 规划棒 · 2026-09-17 16:36–16:5x · 会合/中继拆分复核 + 在册收尾定序)
|
|||
|
|
|
|||
|
|
### 12.1 结论(1 行)
|
|||
|
|
|
|||
|
|
**① 会合 / 中继从 Manager 拆分 ⇒ 判「不做」**(四个耦合点 C1–C4 + 两步改造 S3/S4 **逐条已被覆盖,未覆盖部分 = 无**)|**② 剩余在册四条已定序并出单** ⇒ 产物 = 工作区根 **`交接单_在册收尾_20260917.md`**(243 行;**§8 前缀指纹 `bab83b7219b2669d5a6e9f1acf782e1f`**,全文 md5 `ca13b9ded3d71268f8b0e56755d7aea4`)= **序 ⑰ 执行棒**(S1 mksess 文档口径 → S2 技能三处同步 → S3 Q5 清理 → S4 幕 4-A 临界项)。⛔ 只出规划:未改码、未动 47/106、未 commit/push、零上抛。
|
|||
|
|
|
|||
|
|
### 12.2 复核判定(① 二选一 → **判「不做」**)
|
|||
|
|
|
|||
|
|
| 靶子 | 现状证据(只读取证) | 判定 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **C1** 会合地址硬编码在 Worker env | `src/config.ts:485-490`:`DSHS_RENDEZVOUS_URL` **优先** / `DSHS_TUNNEL_TARGET` 降**兜底**(注释原文「覆盖网络 S1:会合地址出 env」) | ✅ 序② P0-2 覆盖 |
|
|||
|
|
| **C2** 中继落点 = Manager loopback + 两端同号 | `src/config.ts:498-500` `DSHS_RELAY_DIAL_PORT_BASE=25000` / `SPAN=1000` / `POOL=64` ⇒ 落点在 **Manager 本机**回环池;`src/net/relay/dialer.ts:11`「Manager 也像 worker 一样**只拨出**一条 wss」 | ✅ **R5** 覆盖 |
|
|||
|
|
| **C3** 可达性登记磨掉「经谁中转」 | `src/net/rendezvous.ts:103-106`「按 `via` 选实现的注册表」+ `Reachability.via`(`:27/:65/:98`)+ `agentBaseUrlOf()` 单一取址入口(`src/net/reachability.ts:99`) | ✅ S2/P2 覆盖 |
|
|||
|
|
| **C4** 控制面 PG 走同一隧道 | 方案 §8.1 实测:106/47 worker env **均无** `DSHS_TUNNEL_STATIC_PORTS` | ✅ **前提不成立** |
|
|||
|
|
| **S3** 回环别名 | 方案 §9.1 自我证伪(`gatewayports no`)⇒ 改「实例端口区间隔离」,且 R5 后「同号」前提消失 | ✅ 已被更强做法替代 |
|
|||
|
|
| **S4** 中继独立成单元 / 会合可换机 / 多实例 | `dshs-relay` 独立单元(47+106,只绑回环 `20080`)+ 序⑥ S8 第二中继 + 序⑦「杀 47 ⇒ 切 106」实测 | ✅ R2 / 序⑥S8 / 序⑦ 覆盖 |
|
|||
|
|
|
|||
|
|
**处置已落**:`会合中继拆分_取证与改造方案_20260916.md` **头部加状态块**(「已完成使命 · 仅存档 · ⛔ 勿再按 S0–S4 开工」+ 逐条覆盖指针),**正文未改**(历史档案属性)。
|
|||
|
|
⚠️ **顺带登记(⛔ 未处置)**:`src/worker/tunnel.ts`(SSH 隧道)现已是**生产死路径**(47 同机不建隧道、106 走 `wss://`)⇒ 是否删 = 独立决策(可能保留作 ssh 兜底变体)。
|
|||
|
|
|
|||
|
|
### 12.3 在册项定序(② → 序 ⑰ 执行棒,写进同一张单)
|
|||
|
|
|
|||
|
|
| 序 | 项 | 为什么在这个位置 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **S1** | mksess 文档口径(「DB 直插」→「PG 直插」) | **必须最先** —— 它改 `skills/**` 正文,**S2 的镜像同步要把它的产物一起带上** ⇒ 二者共用**一次 scp**(硬顺序) |
|
|||
|
|
| **S2** | 技能 `dsh-auto-handoff-chain` 三处同步 | 紧随 S1(同面、共用一次 scp);它是**自动接续链路**载体 ⇒ 影响后续每一棒的可复现性 |
|
|||
|
|
| **S3** | Q5 `.bak-seq7-*` 清理 | 与 S1/S2 无依赖,同属「零服务副作用批」⇒ 合批省一次抢锁 |
|
|||
|
|
| **S4** | §8.8-2 临界项(幕 4-A) | **唯一动服务项** ⇒ 置末;收尾 `restart dshs` 归零不影响已完成工作 |
|
|||
|
|
|
|||
|
|
**排序一句话**:`S1 → S2`(硬依赖)→ `S3`(零风险清理)→ `S4`(唯一动服务,置末)。
|
|||
|
|
|
|||
|
|
### 12.4 本棒取证(只读 3 条命令族;⛔ 未连任何池口、未动 47/106)
|
|||
|
|
|
|||
|
|
| # | 命令(`cwd` = `D:/github/dsh_shenxian`) | 关键输出 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 1 | `grep -rn "DSHS_TUNNEL_TARGET\|32022" --include=*.ts src` + `grep -rn "DSHS_RENDEZVOUS_URL" --include=*.ts src` | C1 现状(新变量优先、旧变量兜底) |
|
|||
|
|
| 2 | `grep -rn "via\|25000\|dialer" src/net/rendezvous.ts src/net/relay/dialer.ts` + `grep -rn "DSHS_RELAY_DIAL" src/config.ts` | C2/C3 现状 |
|
|||
|
|
| 3 | `find src test scripts -name "*.bak-*"` + `git ls-files src/net/relay \| wc -l` + `git ls-files --error-unmatch <bak>` | 🔴 **Q5 真实范围 = 5 个(非 3 个)**;`src/net/relay` tracked = **0**(Q4 自证) |
|
|||
|
|
|
|||
|
|
🔴 **本棒两条新事实(写进单里,均已自决处置)**:
|
|||
|
|
|
|||
|
|
1. **Q5 范围修正:3 → 5** —— 除原单记的 `src/net/relay/{client,directory,main}.ts.bak-seq7-20260917-112114` 外,**还有 2 个同批残留**:`src/web/server.ts.bak-seq7-20260917-112114`、`src/worker/tunnel.ts.bak-seq7-20260917-112114`。**5 个全部 untracked**(`git ls-files --error-unmatch` 报 pathspec 不匹配)⇒ 删除只减 untracked、不动版本库基线。已按 D2 **扩到 5 个**(同批、同类、**均为过期快照 ⇒ 无回滚价值**)。
|
|||
|
|
2. **`04-调整方案/**` 里的 mksess 旧口径不改** —— 档案属性是**当时事实**(当时确实直插 SQLite),改写 = 销毁溯源。已按 D1 只改操作性载体(`02-运维手册.md` + `skills/**` + 本机技能 10 行),在运维手册 R4 段写一行勘误指针。
|
|||
|
|
3. **幕 4-A 判据取三档递进**(D3):`p95 ≤ 24 s = 健康`|`24–27 s = 临界(记录、⛔ 不判 FAIL)`|`> 27 s = 回头条件触发` —— 观测样本 19.5/20.8/22.2 s,把「假红 vs 预警提前量」的取舍**消解**掉。
|
|||
|
|
|
|||
|
|
### 12.5 越界自证
|
|||
|
|
|
|||
|
|
| 门 | 自证 |
|
|||
|
|
|---|---|
|
|||
|
|
| **D1** | 参数表(截断口径)仍 **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变**)|⛔ 未动 `RELAY_FAILOVER_*` / `HB_SEC` / burst |
|
|||
|
|
| **R7** | 本棒**零代码改动、零服务器改动**(只新增 1 份单 + 给 1 份存档方案加状态块)|⛔ 未 commit / push(HEAD 仍 `640813e`)|Q4 / Q5 **只报告未动手** |
|
|||
|
|
| **COOLDOWN** | ⛔ 本棒未触任何演练 / 夹具路径;单内已把 🔴 禁令原文写进 §3.2 |
|
|||
|
|
| **ssh** | 本棒**未对 47 / 106 执行任何命令**(纯本地只读)⇒ 无 `-p 22` 相关风险面 |
|
|||
|
|
|
|||
|
|
### 12.6 在册未办(原样转 §12.8 的下一棒;⛔ 只报告)
|
|||
|
|
|
|||
|
|
| 序 | 状态 |
|
|||
|
|
|---|---|
|
|||
|
|
| **Q4** | `src/net/relay/**` untracked 留档缺口 —— 🔴 **卡在「提交 / 推送授权」**(实测 `git ls-files src/net/relay` = **0**)⇒ ⛔ **未写进执行范围**,单内 §0.4 只登记一行 |
|
|||
|
|
| **Q5** | 已**定序进 S3**(范围修正为 5 个) |
|
|||
|
|
| **§8.8-1** | 106 sshd MaxStartups 限流(外部爆破)⇒ 脚本化 ssh 偶发 `rc=255`;⛔ 不放宽(命中 R5)⇒ 仍在册 |
|
|||
|
|
| **§5.10 三项** | presence / 房间层 / 内容分发 —— ⛔ 属业务优先级,单内 **§3.3 只登记候选与优缺点、不定序** |
|
|||
|
|
|
|||
|
|
### 12.7 指纹(收口现取)
|
|||
|
|
|
|||
|
|
| 项 | 值 |
|
|||
|
|
|---|---|
|
|||
|
|
| **本棒产物** | `交接单_在册收尾_20260917.md` = **243 行**;全文 md5 **`ca13b9ded3d71268f8b0e56755d7aea4`**;**§8 前缀指纹 `bab83b7219b2669d5a6e9f1acf782e1f`**(口径 = `sed '/^## §8 回报格式/,$d' … \| md5sum`);行尾 = **纯 LF**(CR=0 自证) |
|
|||
|
|
| 参数表(不计入本单口径) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证) |
|
|||
|
|
| 被加状态块的存档方案 | `会合中继拆分_取证与改造方案_20260916.md`(头部新增状态块,**正文未改**;原 278 行) |
|
|||
|
|
| 本交接单 §8 前缀 | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— §12 在 §8 之后,不进前缀口径) |
|
|||
|
|
| 本机技能 mksess 命中 | 文档库 skills = **10 行** / 本机技能 = **10 行**(同数 ⇒ 两副本同步态;**待 S1 同改**) |
|
|||
|
|
| **下一棒** | automation **`067b0892-964e-4deb-bf84-4f1c37bebea8`**(「覆盖网络线-序17执行棒-在册收尾(mksess口径/技能三处同步/Q5清理/幕4A临界项)」,一次性,`scheduledAt` = 2026-09-17 **16:44**,`nextRunAt` = 1789634640000) |
|
|||
|
|
|
|||
|
|
### 12.8 下一棒(序 ⑰ · 执行棒)
|
|||
|
|
|
|||
|
|
**开工依据 = 工作区根 `交接单_在册收尾_20260917.md`(§8 前缀指纹 `bab83b7219b2669d5a6e9f1acf782e1f`)**,按 §5 的 **S1 → S2 → S3 → S4** 顺序执行。
|
|||
|
|
**已登记**:automation **`067b0892-964e-4deb-bf84-4f1c37bebea8`**(一次性,`scheduledAt` = 2026-09-17 **16:44**,`nextRunAt` = 1789634640000)⇒ 已用陈述句告知用户。
|
|||
|
|
**两条硬门**:**D1**(不改任何生产值)|**R7**(Q4 ⛔ 只报告、⛔ 不 commit/push;范围外先报告后动手)。
|
|||
|
|
🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**。
|
|||
|
|
**已定项(⛔ 不必再上抛)**:D1 只改操作性载体(档案不动)|D2 Q5 扩到 5 个|D3 幕 4-A 三档判据|D4 不处理 `tunnel.ts` 死路径|D5 触发回头条件时停下报告、不调生产值。
|
|||
|
|
**收口必须**:释放锁 + **陈述句**告知 + 登记下一棒(id 取自工具返回值)+ 推进 `接续入口_覆盖网络线_20260916.md` §0/§2 + 写工作区日志 + 在本单回填 §8。⛔ 不 commit / 不 push。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §13 序 ⑳ 新增在册缺陷(2026-09-17 18:2x–19:2x · ⛔ 本棒只登记、未动手)
|
|||
|
|
|
|||
|
|
> 本棒在**量化 presence 真机收益**时新查证两条。两者均按 **R7** 处理:**先报告、后动手**。
|
|||
|
|
> 完整取证、命令原文与判定 = 交接单 `交接单_presence在线态_20260917.md` **§8.10**。
|
|||
|
|
|
|||
|
|
| # | 缺陷 | 证据(可复现) | 影响面 | 判定 |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| **P-1** | **`presenceFresh()` 的新鲜度判据与"稳态零帧"互相打架** ⇒ 订阅生效期间 `/status` 轮询**照旧在跑**,presence 的**核心收益未兑现** | ① 判据 = `state==='subscribed' && now − max(lastSnapAt,lastPushAt) ≤ PRESENCE_TTL_MS`(`src/net/relay/client.ts:593-600`)② 稳态 `pushed/snaps` 自 relay 重启起**恒为 1**(**一帧都不推**)⇒ 45 s 后**必然过期** ⇒ 门重开 ③ 实测 `R_now ≈ 10.9 次/分钟` vs 基准 `R_base = 60 000 / RELAY_STATUS_POLL_MS(5000) = 12 次/分钟` ⇒ **降幅 1.10×** ④ 门开关时间线:35 min 内 **11 次切换**、挂起窗口 7/20/20/10/45 s ⇒ **占空比 5.0%**(理论降幅 1.05×,与实测吻合) | 全平台(Manager→relay 的读取量≈无变化);**用户不可见** ⇒ 属**成本 / 设计目标**项 | **另立单**(技术项,下一棒可自决) |
|
|||
|
|
| **P-2** | `src/web/server.ts#translateEndpoint` **键口径不一致 ⇒ 整个闭包是死分支**(`via='relay'` 的实例面翻译**从不生效**) | ① `:733 hostVia.set(name, row.via)`,`name = logicalName(networkId, row.id)` = `ops/w-106`(键 = **逻辑名**)② `src/supervisor/remote-spawner.ts:321 this.translateEndpoint(host.hostId, raw)` 传的是**裸 hostId** ⇒ `:831 hostVia.get(hostId)` **恒 `undefined`** ⇒ 早退原样透传 ③ `:840` 的 `${hostId}:${ep.port}` 快照回退**键同样错**(对照 `:689` / `:700` 用的就是逻辑名) | 仅 `via='relay'` 的**实例面**(今天 = `w-106`);现场 `ss -lntp \| grep -c ':21000'` = **0**、`journalctl -u dshs --since -6h \| grep -c ':21000'` = **0** ⇒ **当前零实际影响** | **另立单**(本棒**无验证条件**:无活跃实例 + 该闭包不可先红后绿,需先抽纯函数) |
|
|||
|
|
|
|||
|
|
**P-1 候选修法(供下一棒选)**:
|
|||
|
|
A) 把**任何入站帧**(含协议心跳)计入新鲜度 —— 最小改动、语义正(连接活着 + 订阅已建立 ⇒ 镜像有效);
|
|||
|
|
B) relay **定期重推 `SNAP`** —— 自愈(漏推可被下一次纠正),代价 = 周期流量;
|
|||
|
|
C) 拉长 `PRESENCE_TTL_MS` —— 只把竞速推远,⛔ 不推荐单用。
|
|||
|
|
**P-2 候选修法**:把翻译判定抽成纯函数 `relayEndpointTarget({via, dialedPort, snapshotLocalPort})` + 建 `hostId → 逻辑名` 映射(`hostNameById`);判据 = `via≠relay` ⇒ 原样透传 / `via=relay` ⇒ ①拨号落点 ②快照落点 ③**`undefined`(失败关闭,⛔ 不原样透传)**;单测至少 4 例,含"传**裸 hostId** 必须命中"的键口径回归。
|
|||
|
|
**两条共同硬门**:**D1**(不改任何生产值)|**R5**(不扩大暴露面)|**R7**(⛔ 不 commit / 不 push)|🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §14 序㉑ 回报(2026-09-17 19:0x–19:4x · **P-1 与 P-2 均已修**;详细取证 = `交接单_presence在线态_20260917.md` **§8.11**)
|
|||
|
|
|
|||
|
|
| # | 项 | 判定 | 结果(一句话) |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **P-1** | `presenceFresh()` 新鲜度判据与"稳态零帧"打架 | ✅ **已修** | 判据改为「**订阅已建立 ∧ 链路活着**」(任何入站帧,含心跳;上界 = `max(TTL, 半开阈值)`)。**候选取 A**:⛔ B(周期 `SNAP`)会**推翻序⑲ E1「稳态 Δpushed ≤ 0」**与 `OBS-14` 的 `snaps ≤ pushed` ⇒ R11 净变差;⛔ C 只把竞速推远。**真机同口径 Before/After**:67 s 窗口 `ΔstatusHits 14 → **1**`(探针自身读数 = 1 ⇒ **Manager 侧 13 → 0**),门开关 **0 次翻转**(重启后 5+ 分钟仅 1 行"挂起")。 |
|
|||
|
|
| **P-2** | `translateEndpoint` 键口径不一致 ⇒ 闭包是死分支 | ✅ **已修(单测面)** | 抽出纯函数 `src/net/relay/endpoint-target.ts`(`relayEndpointTarget` + `hostNameIndex`)+ 闭包先换逻辑名(`hostNameById`);`T35` 四支判定(含"⛔ 不误触发拨号池")、`T36` 键口径 + **源码级守卫**(⛔ 不得再出现裸 `hostId` 作键)。**E2E 未做**(无活跃实例 ⇒ 按入口授权"只做到单测",可执行判据已写进 presence 单 §8.11-⑤)。 |
|
|||
|
|
| **新** | `OBS-16`(门的验收判据) | ➕ **落地** | ⛔ 不能只看 `OBS-13`(它的 `ΔstatusHits ≥ 1` 由探针自身两次读即满足 ⇒ 旧口径下轮询照旧在跑也全绿);新增第三次采样 + `PRESENCE_GATE_WINDOW_MS` / `PRESENCE_GATE_HITS_MAX`;夹具两侧实证 PASS(Δ=1, rc=0) / FAIL(Δ=12, rc=1);真机 PASS。 |
|
|||
|
|
| **新** | **P-2b**(本棒只登记、⛔ 未动手) | ⏳ 在册 | `relayEndpointTarget` 只有"拨号落点 → relay 快照"两级;**P-1 修好后快照路径趋冷**,若拨号池分不出槽位而**订阅推送里有落点**仍会判失败关闭 ⇒ 候选 = 把 `presenceLocalPort` 插成 ②'级(与 `addressOf` 三级链对齐)。属**行为面新增** ⇒ 按 R7 先报告后动手。 |
|
|||
|
|
|
|||
|
|
**零回归三件套**:`npm test` **174/173/0/1**(基线 170/169/0/1 + 新 T33–T36)|`--scene all` **12 PASS / 0 SKIP / 0 FAIL**|`overlay-probe` **15 PASS / 0 FAIL / 1 SKIP**(基线 14/0/1 + `OBS-16`)。
|
|||
|
|
**边界自证**:⛔ 未改任何生产值(`RELAY_FAILOVER_*` / `HB_SEC` / burst;`PRESENCE_TTL_MS` 值未改)|⛔ 未新增公网口 / 未改 nft·nginx|🔴 `COOLDOWN_MS=0` 计数 **0**|⛔ 未 commit / 未 push。
|
|||
|
|
**指纹**:参数表 **`42238175d84319ada99afa56d583f9db`**(旧 `659c08949c193102f774822905fde3b7`)|探针 `61a9fc5623bb617c802daf604d3ce0f3`(旧 `4cbe34f1ffc51cfab78f0540224aa388`)|🔴 **本单指纹改记「前缀口径」**(⛔ 不记全文 —— §13/§14 自身就是回填产物,全文 md5 必然**自指过时**):`sed '/^## §13 序 ⑳ 新增在册缺陷/,$d' 交接单_观测口径与在册缺陷_20260917.md | md5sum` = **`fc0389d759c57a14d038b6450a36d8e1`**(**§13 之前**的正文;回填 §13/§14 **不影响**它,可复现)|收口时全文 md5 实测 = `28bbe2a7467569a0ad76a486ae37546d`(**814 行**、CR=0),⚠️ **仅作当次快照,⛔ 别当复现判据**(旧记的 `5495f2d2360aea64e763ec0e5aeb2987` 是"写完 §14 但尚未补 §13 的 P-2b 行"那一瞬的值,⚠️ 已作废)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §15 序 ㉒ 回报(2026-09-17 19:33–20:0x · **P-2b 已修 + P-1 长窗复测通过**;详细取证 = `交接单_presence在线态_20260917.md` **§8.12**)
|
|||
|
|
|
|||
|
|
| # | 项 | 判定 | 结果(一句话) |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **P-2b** | `relayEndpointTarget` 候选链只有两级 ⇒ 订阅推送里有落点也会判失败关闭 | ✅ **已修** | 补 **②' 级 `pushedLocalPort`**,链变**三级**且与 `addressOf` **逐级对齐**;改动**只在 3 个在册文件**(`endpoint-target.ts` / `web/server.ts` / `test/relay.test.mjs`);**先红后绿**(产物里去掉 ②' 级 ⇒ `T37` actual `{41000,'snapshot'}` vs expected `{37057,'pushed'}`);新增 **T37**(三级逐支 + `0` 哨兵逐级下探 + ⛔ 不碰拨号池)/**T38**(源码级守卫:闭包必须把 `presenceLocalPort(name, ep.port)` 传进判定)。 |
|
|||
|
|
| **P-1(长窗)** | 序㉑ 只验过 **67 s 短窗** | ✅ **长窗通过** | 窗口 **706 s**(11.77 min):`ΔstatusHits 9→10` ⇒ **Manager 侧 = 0**;门开关窗口内 **0 行**;`Δpushed=0` / `Δsnaps=0`。相对基准 `60 000/5 000 = 12 次/分钟`:raw **0.085 次/分钟** ⇒ **141.2×**(Manager 侧零命中 ⇒ 无穷倍)。 |
|
|||
|
|
| **新** | 环境勘误:106 的 ssh 口径 | 🔴 **已定** | **必须用别名 `test106`**(专用密钥 `id_ed25519_test106`)。裸 `ssh [email protected]` ⇒ `Permission denied (publickey)` —— ⛔ **别再误判成 MaxStartups 限流**(本棒先误判、后取证纠正)。 |
|
|||
|
|
| **遗留** | E2E(P-2 + P-2b) | ⏳ **未做** | 无活跃实例(47 上 `dsh-systemctl list-units 'dsh-*.scope'` = **0**)⇒ 判据仍留 presence 单 **§8.11-⑤**,交 **序 ㉓**。 |
|
|||
|
|
|
|||
|
|
**在册缺陷表(现行)**:**P-1 ✅ / P-2 ✅(单测面)/ P-2b ✅** —— **无未关闭在册缺陷**;剩余仅 **`§9-10`「relay 侧静默丢订阅而 socket 仍活 ⇒ 门察觉不到」**(当前代码路径**不可达**,属**回头条件**而非在册缺陷)+ **E2E 缺口**。
|
|||
|
|
**零回归三件套**:`npm test` **176/175/0/1**(基线 174/173/0/1 + T37/T38)|`--scene all` **12 PASS / 0 SKIP / 0 FAIL**|`overlay-probe` **15 PASS / 0 FAIL / 1 SKIP**(**部署后重跑同值**)。
|
|||
|
|
**部署 + 回滚点**:`lib/net/relay/{endpoint-target,index}.js` + `lib/web/server.js` 铺 **4 处**(47 `/opt/dshs/lib`+`/opt/dsh-relay/lib`、106 `/opt/dshs-cluster/lib`+`/opt/dsh-relay/lib`),md5 四根全同(`36b2dd99…` / `486419ba…` / `460d360e…`);**只重启 47 的 `dshs`**;回滚点 = 两机 **`/opt/dsh/backups/seq22-20260917-195602/`**(旧 md5 `57e6c9ae…` / `486419ba…` / `74c077b3…`,由"反向改回两处 hunk → `tsc`"**精确重放**取得)。
|
|||
|
|
**边界自证**:⛔ 未改任何生产值(`RELAY_FAILOVER_*` / `HB_SEC` / burst 逐项未变)|⛔ 未新增公网口 / 未改 nft·nginx|🔴 `COOLDOWN_MS=0` 计数 **0**|⛔ 未 commit / 未 push。
|
|||
|
|
**指纹**:参数表 **`42238175d84319ada99afa56d583f9db`(未变 ✅ —— 本棒无数值改动)**|探针 `61a9fc5623bb617c802daf604d3ce0f3`(未变)|本单 §13 前缀口径 `fc0389d759c57a14d038b6450a36d8e1`(**未变 ✅ —— §15 在截断点之后**)|代码面:`endpoint-target.ts` `9f421a9f7bdff18cd02fd60cf4add2fb`、`web/server.ts` `8c4c072cb7dc068b4f378f51b5d5a29a`、`test/relay.test.mjs` `f08088b8c07bf7f18a0c0ce0a98ee018`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §16 序 ㉓ 回报(2026-09-17 20:08–20:4x · **E2E 已验收 + 在册缺陷表清空 + 线内可停**;详细取证 = `交接单_presence在线态_20260917.md` **§8.13**)
|
|||
|
|
|
|||
|
|
| # | 项 | 判定 | 结果(一句话) |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **E2E(P-2 + P-2b)** | 序㉑/序㉒ 都只做到单测 | ✅ **已验收** | **R4** 临时 session ⇒ `POST /api/dsh/enter`:ⓐ **200**(同连接双发 + 6 轮复测全 200)ⓑ 47 上 `:21000` 监听 **0**(`:19000` 亦 0)ⓒ `overlay-endpoint` 窗口内 **0 行**(失败关闭未触发)ⓓ relay `counters.dial` **0 → 2**。**判别器更强证据**:拨号原文 `落点 127.0.0.1:25000 -> ops/w-106:19000`(agent 面)+ `落点 127.0.0.1:25001 -> ops/w-106:21000`(实例面)⇒ 键口径(逻辑名)与三级候选链**都在生产上走通了**;同时 106 上 `dsh-100002-…scope` argv 末尾 `--port 21000` 在听 ⇒ **"47 无 21000 / 106 有 21000" 同时成立** = 翻译生效强判据。 |
|
|||
|
|
| **用户可见面** | 序⑬ Q2(guest(w-106) 实例页 502) | ✅ **判据已满足** | `curl -H 'Host: guest.alotbuy.com' 127.0.0.1:3080/` ⇒ **200**、**62451** 字节真 HTML、`502` 标记 **0**。 |
|
|||
|
|
| **新** | `OBS-09` 由 SKIP → **PASS** | ✅ **回头条件兑现** | `PASS OBS-09 在册实例面 w-106:37747=401` ⇒ **序⑲ §9-8 的回头条件已兑现**("有用户访问实例后 `OBS-09` 应回 PASS"),⛔ **不是**真回归。探针整表 **16 PASS / 0 FAIL / 0 SKIP**(基线 15/0/1 ⇒ **+1 better**)。 |
|
|||
|
|
| **新登记(R7 · ⛔ 未动手)** | **O-1**:`model landing failed, falling back to env injection Error: ENOENT … /var/lib/dshs/users/<uid>/home/.credentials.yaml` | ⏳ 线外 | **用本地路径读远端实例 home**(与序⑬ Q2 所记真因**同族**);有 env 注入兜底、**用户可见面仍 200**;归属 = `RemoteSpawner.resolveApiKey`(平台模型落地面)⇒ **不在覆盖网络线 §3.1 文件集**,仅登记、待定立项。 |
|
|||
|
|
| **观察项** | `OBS-15` 在线态陈旧 p95 | ✅ 未越界 | 本棒 **1770 ms**(序㉒ 曾见 14220 ms,阈值 15000 ms)⇒ 抖动主导,⛔ 未改阈值。 |
|
|||
|
|
|
|||
|
|
**在册缺陷表(现行)**:**P-1 ✅ / P-2 ✅ / P-2b ✅ + E2E ✅** ⇒ **空**。剩余仅 **`§9-10`**(relay 静默丢订阅 ⇒ 回头条件,当前代码路径**不可达**)+ **三项待拍板**(`B` 内容分发 / 「Manager 重启后自动拉起实例」/ 入口 §4 骨干服务范围)。
|
|||
|
|
**收口复核结论**:**覆盖网络线线内可停** ⇒ 按登记门禁(用户 2026-09-17 明令)**本棒不登记下一棒**,等拍板后再建。
|
|||
|
|
**零回归三件套**:`npm test` **176/175/0/1** | `--scene all` **12 PASS / 0 SKIP / 0 FAIL**(`rc=0`)| `overlay-probe` **16 PASS / 0 FAIL / 0 SKIP**(`rc=0`)。
|
|||
|
|
**边界自证**:⛔ 未改任何代码(in-册三个文件 md5 与序㉒ **全同**)|⛔ 未改任何生产值(参数表指纹 **未变**)|⛔ 未动 nft·nginx / 未新增公网口|🔴 `COOLDOWN_MS=0` 计数 **0**|⛔ 未 commit / 未 push|R4 临时 session 残留 **0**。
|
|||
|
|
**指纹**:参数表 **`42238175d84319ada99afa56d583f9db`(未变 ✅)**|探针 `61a9fc5623bb617c802daf604d3ce0f3`(未变)|本单 §13 前缀口径 `fc0389d759c57a14d038b6450a36d8e1`(**未变 ✅ —— §16 在截断点之后**)|代码仓 HEAD `04776af`(改动数 10,与 T0 一致)。
|