feat(overlay): 内容块级寻址 + 实例逐步拉起 + 骨干选路 + 组密钥加密(序24–㉛ 累积同步)

代码
- 内容分发块级寻址:新增 src/net/relay/content/{chunker,store,runtime,source,peer,crypto}.ts
- 组密钥(C 档)确定性加密:AES-256-GCM,块 id β′ = sha256(密文) 前 32 hex;双 epoch 过渡窗口
- 实例生命周期:三处 teardown() 不再杀实例(local/remote/leased-spawner);启动认领 + TCP 探活判孤儿
- 骨干选路:jitter 选路 + endpoint-target;relay client/server/wire/identity/directory/rendezvous/switcher 调整
- 工作台 src/web/server.ts、src/worker/relay-tunnel.ts 装配与候选链观测

脚本与测试
- scripts/overlay-{probe,keyring,jitter}.cjs 更新
- 探针新增 OBS-21(每连接候选数)/ OBS-22(teardown 静态守卫 + 认领面)/ OBS-23(组密钥加密)
- 新增 test/{orchestrator-teardown,orchestrator-rehydrate,overlay-content,overlay-jitter}.test.mjs;relay 两例更新

文档
- 新增交接单:覆盖网络-序24-内容分发块级寻址 / 序25-实例逐步拉起 / 序26-骨干稳定选路与加密
- INDEX.md、交接单/README.md、skills/dsh-auto-handoff-chain/SKILL.md 同步

验收(零回归,2026-09-18 08:0x 复核)
- npm test           201 tests / 200 pass / 0 fail / 1 skipped
- overlay-failover-drill --scene all --table   12 PASS / 0 SKIP / 0 FAIL
- overlay-probe --table                        23 PASS / 0 SKIP / 0 FAIL (rc=0)
This commit is contained in:
admin committed 2026-09-18 08:08:51 +08:00
1 parent 04776af4b1
commit 09ce76f3af
38 files changed
+9133 -211

No files matched your search

+112
View File
@@ -8,10 +8,21 @@
* 而 336 ms 目前是"跨云链路很差"的**唯一证据** ⇒ 若它是口径问题,后面所有
* "跨云不可玩"的结论都要重判。本脚本做**三方对比**:ICMP / TCP 握手 / relay 心跳。
*
* ## 序㉖ 增补:`--watch` **持续采样**(点测 → 连续观测)
*
* 序㉖ 的选路主序是 **jitter**(`src/net/relay/jitter.ts`)⇒ 运维侧必须能**连续**看每条候选
* 路径的 jitter,否则"选路按 jitter"这件事在生产上是**不可见证**的。
*
* 🔴 **本脚本⛔ 不自带统计实现**:它 `require('../lib/net/relay/jitter.js')` 复用
* `absDeltas` / `percentile` / `histogram` / `JitterTracker` / `orderByJitter` /
* `pickJitterTarget` —— 与产品路径**同一份算法、同一份阈值(`JITTER_*` env)**。
* 本线教训:「另一份实现 = 另一处静默失效」(取址链踩过两次)。
*
* 用法:
* node overlay-jitter.cjs --icmp 106.54.21.172 --count 300 --interval 0.2
* node overlay-jitter.cjs --tcp 106.54.21.172:22 --tcp-n 30
* node overlay-jitter.cjs --icmp H --count 300 --tcp H:22 --tcp-n 30 # 两者一起跑
* node overlay-jitter.cjs --watch --targets 106.54.21.172:22,47.77.182.89:22 --rounds 40 --gap 400
*
* @module scripts/overlay-jitter
*/
@@ -111,6 +122,100 @@ function tcpHandshake(host, port, n) {
})
}
/**
* 单次 TCP 握手 RTT(ms);失败 ⇒ `undefined`(**不记 0** —— 0 会被当成"极稳"污染排序)。
* ⚠️ 这是 I/O,不是统计;统计一律交给 `lib/net/relay/jitter.js`。
*/
function tcpSampleOnce(host, port) {
return new Promise((resolve) => {
const t = process.hrtime.bigint()
const s = net.connect(port, host)
let settled = false
const done = (v) => {
if (settled) return
settled = true
s.destroy()
resolve(v)
}
s.setTimeout(5000)
s.on('connect', () => done(Number(process.hrtime.bigint() - t) / 1e6))
s.on('error', () => done(undefined))
s.on('timeout', () => done(undefined))
})
}
/**
* 序㉖ `--watch`:**持续采样 + 按产品同一份算法给出选路序**。
*
* 输出里的 `order` 就是 `orderByJitter()` 的结果 ⇒ 与 `directory.ts` 生产选路**同函数**。
*/
async function watch(targetsRaw, rounds, gap) {
const path = require('node:path')
const J = require(path.join(__dirname, '..', 'lib', 'net', 'relay', 'jitter.js'))
const th = J.jitterThresholds()
const tracker = new J.JitterTracker(th)
const urls = targetsRaw.map((s) => String(s))
const counters = new Map(urls.map((u) => [u, { ok: 0, fail: 0, rtts: [] }]))
const N = Math.max(2, rounds || 40)
for (let r = 0; r < N; r++) {
for (const u of urls) {
const [h, p] = u.split(':')
const rtt = await tcpSampleOnce(h, Number(p || 22))
const c = counters.get(u)
if (rtt === undefined) c.fail += 1
else {
c.ok += 1
c.rtts.push(Math.round(rtt * 100) / 100)
tracker.record(u, rtt)
}
}
if (gap > 0 && r < N - 1) await new Promise((res) => setTimeout(res, gap))
}
const per = {}
for (const u of urls) {
const c = counters.get(u)
const st = tracker.stats(u)
const sorted = [...c.rtts].sort((a, b) => a - b)
per[u] = {
samples: c.ok,
failed: c.fail,
rttMedianMs: sorted.length === 0 ? undefined : sorted[Math.floor(sorted.length / 2)],
rttP95Ms: sorted.length === 0 ? undefined : sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))],
/** ⚠️ `undefined` = 差分样本 < `JITTER_MIN_SAMPLES` ⇒ **未知**,⛔ 不当 0 用。 */
jitterMs: st === undefined ? undefined : st.p95AbsDeltaMs,
deltas: st === undefined ? 0 : st.deltas,
hist: st === undefined ? undefined : st.hist,
}
}
const order = J.orderByJitter(urls, tracker)
const first = order[0]
const curJ = per[first] && per[first].jitterMs
const pick =
curJ === undefined
? undefined
: J.pickJitterTarget({ urls, tracker, curUrl: first, curJitterMs: curJ, switchMs: th.switchMs })
return {
probe: 'tcp-handshake',
thresholds: th,
rounds: N,
gapMs: gap,
targets: per,
order: [...order],
/** 主判据 E1 的读法:`order` 首位 = jitter 最低者(**可能与 RTT 最低者不是同一条**)。 */
e1: {
curUrl: first,
curJitterMs: curJ,
rttLowestUrl: urls.slice().sort((a, b) => (per[a].rttMedianMs ?? 1e9) - (per[b].rttMedianMs ?? 1e9))[0],
note: 'order 首位由 jitter 决定;若 rttLowestUrl !== order[0] ⇒ 直接印证"看 jitter 不看 RTT"',
wouldSwitchTo: pick === undefined ? null : pick,
},
}
}
async function main() {
const res = { host: require('node:os').hostname(), platform: process.platform }
if (args.icmp) res.icmp = icmp(String(args.icmp), args.count || 300, args.interval)
@@ -118,6 +223,13 @@ async function main() {
const [h, p] = String(args.tcp).split(':')
res.tcpHandshake = await tcpHandshake(h, Number(p || 22), args['tcp-n'] || 30)
}
if (args.watch) {
if (typeof args.targets !== 'string') {
log('--watch 需要 --targets host:port[,host:port...]')
process.exit(2)
}
res.watch = await watch(args.targets.split(','), args.rounds, args.gap === undefined ? 400 : args.gap)
}
out(res)
process.exit(0)
}