交接单按归属约定(正文落文档库、工作区只放指针)归档至 归档/交接单-20260924-归档/,含逐件判定 README: - 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26) - 4 件主题已被覆盖网络线入口汇总 - 7 件历史接续包/规划件 CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、 工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
93 KiB
交接单 · 观测口径重构(OBS-11 计数 → 白名单集合)+ 在册小缺陷清算排序(2026-09-17)
- 状态:⏳ 待执行(规划棒产物,2026-09-17 14:5x 出单)
- 出单者:覆盖网络线-序11规划棒(automation
3d4dffc0-356e-4bc6-9628-b7d664da7db9) - 执行方式:规划与执行分离 ⇒ 执行会话只按本单开工、不读规划会话上下文
- 上游依据(现取证据,⛔ 不必重跑):
交接单_检测时延与deadline_20260917.md§8.8-4 ③ 逐口对账(序⑩ 收口回填)—— OBS-1178→77的名字 =20000(47 实例档、无活跃实例)- 同单 §8.8-4 末段「🔴 ③ 的副产品」 —— 计数判据的两处结构缺陷(ⓐ 对状态敏感 ⇒ 假红|ⓑ 对替换式变化不敏感 ⇒ 假绿)
参数表_覆盖网络_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 决策点
已定(本规划棒自决,可推翻)
- 判据形态 = 三集包含式集合判据(不是"计数 + 容差",也不是"只报不判"):
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让合法动态落点有名字 ⇒ 替换式变化(一进一出)必然被两条断言之一命中。
LISTEN_COUNT的"有/无实例态"两值 —— ⛔ 不表达,直接消除:实例档20000(w-47)/21000(w-106)进允许区间 ⇒ 有实例则出现(合法)、无实例则不出现(不红)⇒ 状态无关化。 ⛔ 不新增LISTEN_COUNT_UP/LISTEN_COUNT_DOWN(那等于把"对状态敏感"这条缺陷编码进参数表 = 净退化,违 R11)。nft同族缺陷一并改造(结论:改,但机制与监听口不同):nft 是内容不是端口 ⇒ 用nft -j入站 accept 集合 ⊆ 白名单(语义判据,比行数更贴近"零新增暴露面"的本意);nft list ruleset | wc -l降级为"仅打印的上下文",不再参与判 PASS/FAIL。⛔ 退化为文本解析时必须显式标记,⛔ 不得静默改判据(text-fallback字样进输出行)。LISTEN_COUNT/NFT_RULES退役为"历史留档":两行留在参数表(供对账),但OBS-11判据列不再引用;退役说明写在参数表 §6 内(单一来源内自解释),⛔ 不追改历史单。- 假绿/假红的自证方式 = 夹具 + 真机受控临时口(§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链(familyip/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,只读 + 一个受控临时口)
- 现态跑一次 ⇒ 期望
OBS-11 PASS,且 stderr 无差集(overlay-probe总数由 10/12 → 11/12,剩余的OBS-09= 环境态)。 - 受控临时口:本机
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 · 连带更新
- 参数表 §6:新增 4 键;
LISTEN_COUNT/NFT_RULES两行后加注「⛔ 已退役(序⑪ 集合判据替代)—— 仅对账用,不得再作为判据」(值保留,历史可比)。 - 参数表 §10 指纹:按表内既有口径重取现值,写进 §10 的"现取"行。
- 入口
接续入口_覆盖网络线_20260916.md§0:加/改 1 行口径指针(指向本单 §8.9 与参数表 §6)。 - ⛔ 不追改历史单(见 §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 的 fail2baninactive⇒ 未认证连接槽位被爆破流量打满 ⇒ 新连接被丢 ⇒ sshrc=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 🟡 与单不符的两处 —— 已按"实质判据"处理(供下一轮修单模板时吸收)
- 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(已复取干净版)。 - 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⇒ PGsessions仍是 2、SQLitesessions变 3 ⇒ 会话落错库、平台读不到(R4 的实例面验收手段整体失效)。 - 改法:实现只有一份 =
/opt/dshs/mksess.cjs—— 连接串从DSHS_DB_URLenv →/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 字符、PGsessions2→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 也复现 ⇒ 与请求体无关。)
取证链(逐层排除):
- 客户端无辜:Manager 侧
strace -f -e trace=write,writev -p <dshs pid>抓到写出的就是一条格式完全正确的请求(POST /fs/isdir HTTP/1.1、content-length: 62、body 完整)。 - 走的就是拨号池口:
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落到已被复用的那条 ⇒ 失败。 - 400 的出处已定死:body 与 Fastify
node_modules/fastify/fastify.js:985的clientError分支逐字一致 ⇒ 这是收到非法字节流时写裸 socket 的兜底响应,⛔ 不是任何业务路由返回的;它不写 pino 日志 ⇒ 这正是"106 worker 日志里什么都没有"的原因。 - 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。
- 全场只有 1 条
- 因此:责任面收敛到 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 agentPOST /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() 把读侧接回了出向。
// 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/ 106dshs-worker未重启(改动是拨号方专属一行,worker/relay 侧行为逐字未变)。 - R7 传播面自证:铺之前对本机
lib/net/relay/*.js与 47/opt/dshs/lib/net/relay/*.js逐文件对账 ⇒ Δ 只有client.js一个文件(无夹带改动)。
- 只重启了 47 的
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→ 500fetch 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:
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)
onConn:⛔ 不再close()服务器,只tcp.destroy()+ 计数(新增stray)+ 点名日志。 ⛔ 不取候选 ②(把槽位从slots摘掉):那等于让任意一条本地连接都能永久蚕食池容量(扫 64 次即可把池扫空)= 净退化(R11);候选 ③(换口段 + 显式状态机)超出src/net/relay/**必要范围。- 纵深防御:
localPortFor()的空槽查找与lruIdle()都只认server.listening的槽;一个可成交槽都没有时**点名"哪几个口已不在听"**再拒(失败关闭,⛔ 绝不发死口号)。 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 自证) |
🔴 本棒两条新事实(写进单里,均已自决处置):
- 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 个(同批、同类、均为过期快照 ⇒ 无回滚价值)。 04-调整方案/**里的 mksess 旧口径不改 —— 档案属性是当时事实(当时确实直插 SQLite),改写 = 销毁溯源。已按 D1 只改操作性载体(02-运维手册.md+skills/**+ 本机技能 10 行),在运维手册 R4 段写一行勘误指针。- 幕 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 一致)。