Files
dsh_shenxian/test/overlay-direct.test.mjs
T
admin 452924d89c feat(config): 涉密内容外置到配置目录(档案 140)
把散落在代码里的真实部署值统一收进 config/,代码改为引用配置,
使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。

新增 config/:platform.env.example(模板)· load.sh(shell 加载器)·
index.cjs(node 加载器)· README.md(键一览与优先级)。
真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。

TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用):
platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。
config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由
DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径,
src/** 注释中性化 116 行/53 文件。

scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径
一律改从配置取;web/wake.html 的注册域白名单改为运行时从
location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737
保留值,并把「内置种子必须为空」固化为回归断言。

取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有);
全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归
(/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
2026-09-19 15:12:19 +08:00

404 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 覆盖网络 **直连(打洞)** 单测(序㊵ · P2 · S5)。
*
* ⚠️ **刻意不进 `npm test`** —— 那是**硬编码文件列表**,加进去会改测试总数(既有口径:
* `test/overlay-join.test.mjs` 同样"刻意不进")。本文件单独跑:
* ```bash
* node --test test/overlay-direct.test.mjs
* ```
*
* | 用例 | 判据 | 断言的东西 |
* |---|---|---|
* | T1 | D3 |开关解析 | 缺省=开;关集/开集;**非法值 ⇒ `null`(⛔ 不静默取缺省)**;本机配置次之 |
* | T2 | D2 |关闭生效 | 关闭 ⇒ **零 UDP socket** + **零候选**(⛔ 不是"少发几条") |
* | T3 | D1 |候选准入矩阵 | 同网+白名单内接受;跨网 / 替第三人申报 / 白名单外 / 本机不在白名单 / 坏形状 / 夹带凭据 / 坏地址 / 超限 / 过期 **各自具名拒绝**;`silentRejections = 0` |
* | T4 | D1 |**准入策略与 server.ts 等价** | 读 `server.ts` 源码核对两处闸门与 `isAllowedDialer` **同策略**(⛔ 防"两处写分叉") |
* | T5 | D5 |打洞成功路径 | NAT 模拟器 + 真 `dgram` ⇒ 双向成立(`punchOk ≥ 1`) |
* | T6 | D5 |**单向不算直连** | 一个方向成立 ⇒ 判死 `one-way`(⛔ 不许把单向当成功) |
* | T7 | D6 |失败判死有界 | 对端不响应 ⇒ 窗内判死,且耗时**有界**(⛔ 不无限重试) |
* | T8 | D6 |冷却 | 判死后二次尝试被挡(**且不开 socket**);`ms = 0` ⇒ **构造即抛** |
* | T9 | D3/D4 |join 回读 + 提示 | 本机配置读回 `direct = true`;`--direct 0` 被尊重 |
* | T10 | D4 |提示可行动 | 三段齐(谁可能连 / 怎么关 / 关掉影响什么),且**含开关键与关值** |
*/
import assert from 'node:assert/strict'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { randomBytes } from 'node:crypto'
import { test } from 'node:test'
import { createSocket } from 'node:dgram'
import { signPayloadWith } from '../lib/net/relay/identity.js'
import {
CandidateLedger,
DEFAULT_DIRECT_COOLDOWN_MS,
DEFAULT_DIRECT_ENABLED,
DIRECT_CAND_MAX_ADDRS,
DIRECT_ENV_KEY,
DIRECT_OFF_VALUES,
DirectCooldown,
DirectPath,
NODES_REGISTRY_VERSION,
PUNCH_PORT_BASE,
PUNCH_PORT_SPAN,
directFromNodeConfig,
directHintLines,
directHintText,
encodeDirectMessage,
generateAuthorityKey,
generateNodeKey,
isAllowedDialer,
isValidAddress,
networkInvitePayload,
newInviteNonce,
nodeJoinIo,
normalizeDialers,
publicKeyOfPrivate,
readNodeConfig,
resolveDirectSwitch,
runJoin,
runPunchAttempt,
runPunchPair,
writeNodeConfigDirect,
} from '../lib/net/relay/index.js'
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const root = fileURLToPath(new URL('..', import.meta.url))
const ssPath = join(root, 'src', 'net', 'relay', 'network.ts')
const serverPath = join(root, 'src', 'net', 'relay', 'server.ts')
/** 真·白名单(**走既有归一化实现** ⇒ 量的是"同一份策略"的行为)。 */
const dialers = () =>
normalizeDialers(
new Map([
['ops', new Set(['manager', 'w-2'])],
['u:5', new Set(['w-2'])],
]),
)
/** 找一个没人监听的本地 UDP 端口(用于产出 `deadline` 负腿)。 */
async function deadPort() {
const s = createSocket('udp4')
await new Promise((r) => s.bind({ port: 0, address: '127.0.0.1' }, r))
const p = s.address().port
await new Promise((r) => s.close(r))
return p
}
const candidate = (over = {}) =>
encodeDirectMessage({
hostId: 'w-2',
network: 'ops',
addrs: [{ host: '10.0.0.9', port: PUNCH_PORT_BASE + 1 }],
ts: Date.now(),
...over,
})
// ── T1 · 开关解析 ────────────────────────────────────────────────────────────────
test('T1 开关:缺省=开|开集/关集|非法值 ⇒ null(⛔ 不静默取缺省)|本机配置次之', () => {
const d = resolveDirectSwitch({})
assert.equal(d.enabled, true, '缺省必须 = 开(用户口径②)')
assert.equal(d.source, 'default')
assert.equal(DEFAULT_DIRECT_ENABLED, true)
for (const v of ['1', 'true', 'ON', 'Yes']) {
const s = resolveDirectSwitch({ [DIRECT_ENV_KEY]: v })
assert.equal(s.enabled, true, `${v} 应判开`)
assert.equal(s.source, 'env')
}
for (const v of DIRECT_OFF_VALUES) {
const s = resolveDirectSwitch({ [DIRECT_ENV_KEY]: v.toUpperCase() })
assert.equal(s.enabled, false, `${v} 应判关`)
}
// 🔴 非法值:**既不许当开也不许当关**
const bad = resolveDirectSwitch({ [DIRECT_ENV_KEY]: 'maybe' })
assert.equal(bad.enabled, null)
assert.match(bad.invalid, /不静默取缺省/)
// 本机配置(用户设置)在 env 缺省时生效;env 显式设了就压过它
assert.equal(resolveDirectSwitch({}, { localDirect: false }).enabled, false)
assert.equal(resolveDirectSwitch({}, { localDirect: false }).source, 'node-config')
assert.equal(resolveDirectSwitch({ [DIRECT_ENV_KEY]: '1' }, { localDirect: false }).enabled, true)
// 空串 = 未设(⛔ 不算非法值)
assert.equal(resolveDirectSwitch({ [DIRECT_ENV_KEY]: ' ' }).enabled, true)
})
// ── T2 · 关闭 ⇒ 零 socket / 零候选 ──────────────────────────────────────────────
test('T2 关闭 ⇒ 零 UDP socket + 零候选;开启才真的开', async () => {
const ctxOf = { dialers: dialers(), from: { network: 'ops', hostId: 'w-2' }, selfHostId: 'manager' }
const off = new DirectPath({ switchState: resolveDirectSwitch({ [DIRECT_ENV_KEY]: 'false' }), deadlineMs: 300 })
const v = off.offerCandidate(candidate(), ctxOf)
assert.equal(v.ok, false)
assert.equal(v.reason, 'disabled')
const a = await off.attempt('ops/void', [{ host: '127.0.0.1', port: await deadPort() }], { sleep })
assert.equal(a.reason, 'disabled')
const st = off.status()
assert.equal(st.counters.udpSocketsOpened, 0, '关闭 ⇒ ⛔ 一个 UDP socket 都不许开')
assert.equal(st.counters.candidatesEmitted, 0, '关闭 ⇒ ⛔ 一条候选都不许发')
// 非法开关值同样**两种动作都不做**(避免"取值写错 ⇒ 静默当成开")
const bad = new DirectPath({ switchState: resolveDirectSwitch({ [DIRECT_ENV_KEY]: 'maybe' }), deadlineMs: 300 })
assert.equal(bad.offerCandidate(candidate(), ctxOf).reason, 'invalid-switch')
assert.equal((await bad.attempt('ops/void', [{ host: '127.0.0.1', port: await deadPort() }], { sleep })).reason, 'invalid-switch')
assert.equal(bad.status().counters.udpSocketsOpened, 0)
const on = new DirectPath({ switchState: resolveDirectSwitch({ [DIRECT_ENV_KEY]: 'true' }), deadlineMs: 300 })
assert.equal(on.offerCandidate(candidate(), ctxOf).ok, true)
await on.attempt('ops/void', [{ host: '127.0.0.1', port: await deadPort() }], { sleep })
assert.equal(on.status().counters.udpSocketsOpened, 1, '开启 ⇒ 真的绑了 1 个 UDP socket')
assert.equal(on.status().counters.candidatesEmitted, 1)
})
// ── T3 · 候选准入矩阵 ───────────────────────────────────────────────────────────
test('T3 候选准入:同网+白名单内接受;其余**逐类具名**拒绝;静默拒绝 = 0', () => {
const led = new CandidateLedger()
const inOps = { dialers: dialers(), from: { network: 'ops', hostId: 'w-2' }, selfHostId: 'manager' }
const expectOk = (raw, ctx = inOps) => {
const v = led.judge(raw, ctx)
assert.equal(v.ok, true, `应接受:${v.ok ? '' : v.reason} ${v.ok ? '' : v.detail}`)
}
const expectReject = (raw, reason, ctx = inOps) => {
const v = led.judge(raw, ctx)
assert.equal(v.ok, false)
assert.equal(v.reason, reason)
assert.ok(typeof v.detail === 'string' && v.detail !== '', '拒绝必须带人读原因(⛔ 不许只回 false)')
}
expectOk(candidate())
expectOk(candidate({ addrs: [{ host: '10.0.0.9', port: PUNCH_PORT_BASE + 1 }, { host: '2001:db8::1', port: PUNCH_PORT_BASE + 2 }] }))
// 同名跨网**互不可见**:同一 hostId 在另一张网里是合法身份
expectOk(candidate({ network: 'u:5' }), { dialers: dialers(), from: { network: 'u:5', hostId: 'w-2' }, selfHostId: 'w-2' })
expectReject(candidate({ network: 'u:5' }), 'cross-network')
expectReject(candidate({ hostId: 'w-999' }), 'not-self-candidate')
expectReject(candidate(), 'not-a-dialer', { ...inOps, dialers: normalizeDialers(new Map([['ops', new Set(['manager'])]])) })
expectReject(candidate(), 'self-not-a-dialer', { ...inOps, selfHostId: 'w-nobody' })
expectReject('{"kind":"DIRECT_CANDIDATE"}', 'bad-shape')
expectReject('not json at all', 'bad-shape')
expectReject(
JSON.stringify({ kind: 'DIRECT_CANDIDATE', hostId: 'w-2', network: 'ops', addrs: [{ host: '10.0.0.9', port: PUNCH_PORT_BASE + 1 }], nodeKey: 'deadbeef' }),
'secret-field',
)
expectReject(candidate({ addrs: [{ host: 'example.com', port: 80 }] }), 'bad-address')
expectReject(candidate({ addrs: [{ host: '10.0.0.9', port: 0 }] }), 'bad-address')
expectReject(
candidate({ addrs: Array.from({ length: DIRECT_CAND_MAX_ADDRS + 1 }, (_, i) => ({ host: '10.0.0.9', port: PUNCH_PORT_BASE + i })) }),
'too-many-addrs',
)
expectReject(candidate({ ts: Date.now() - 10 * 60_000 }), 'stale')
const snap = led.snapshot()
assert.equal(snap.accepted, 3)
assert.equal(snap.rejected.length, 11, '负腿 11 条:跨网/替第三人/白名单外/本机不在白名单/坏形状×2/凭据字段/坏地址×2/超限/过期')
assert.equal(snap.silentRejections, 0, '静默拒绝必须为 0(本线老病根的不变量守卫)')
assert.equal(snap.received, 14)
// 每一条拒绝**都有具名原因**
for (const r of snap.rejected) assert.ok(typeof r.reason === 'string' && r.reason !== '')
// 白名单是**按网络分桶**的:拿 ops 的桶查 u:5 的同名 hostId 必须为假
assert.equal(isAllowedDialer(dialers(), 'ops', 'w-2'), true)
assert.equal(isAllowedDialer(dialers(), 'u:5', 'w-2'), true)
assert.equal(isAllowedDialer(dialers(), 'u:7', 'w-2'), false)
assert.equal(isAllowedDialer(dialers(), 'ops', 'w-999'), false)
// 地址形状:IPv4/IPv6 收,主机名不收
assert.equal(isValidAddress({ host: '10.0.0.9', port: 21100 }), true)
assert.equal(isValidAddress({ host: '1.2.3', port: 21100 }), false)
assert.equal(isValidAddress({ host: '256.1.1.1', port: 21100 }), false)
assert.equal(isValidAddress({ host: 'a.b.c', port: 21100 }), false)
})
// ── T4 · 准入策略与 server.ts 的等价性(机器守卫) ──────────────────────────────
test('T4 准入策略**复用** server.ts 的那一条(⛔ 防两处写分叉)', () => {
const netSrc = readFileSync(ssPath, 'utf8')
const srvSrc = readFileSync(serverPath, 'utf8')
// ① 唯一出口确实在 network.ts 里,且语义就是"该网桶里有没有这个 hostId"
assert.match(netSrc, /export function isAllowedDialer\(/)
assert.match(netSrc, /return map\.get\(network\)\?\.has\(hostId\) === true/)
// ② server.ts 的两处闸门与它**同策略**(同 map 、同 `=== true` 默认拒绝)
const registerGate = srvSrc.includes('const wantDialer = this.dialers.get(network)?.has(hostId) === true')
const dialGate = srvSrc.includes('if (this.dialers.get(session.network)?.has(session.hostId) !== true) {')
assert.ok(registerGate, '注册闸门(server.ts)应仍是 dialers.get(network)?.has(hostId) === true(⛔ 本棒不许改它的语义)')
assert.ok(dialGate, 'DIAL 闸门(server.ts)应仍是 dialers.get(session.network)?.has(session.hostId) !== true')
// ③ 直连模块**不许**自己再判一遍(⛔ 防"同一事实两处写")
for (const f of ['direct/candidate.ts', 'direct/index.ts', 'direct/punch.ts']) {
const src = readFileSync(join(root, 'src', 'net', 'relay', f), 'utf8')
assert.ok(!/dialers\.get\(/.test(src), `${f} 里出现了 dialers.get( ⇒ 疑似把白名单判定抄了一份`)
assert.ok(!/\.has\(hostId\)/.test(src), `${f} 里出现了 .has(hostId) ⇒ 疑似把白名单判定抄了一份`)
}
// ④ 等价性:默认拒绝(空桶 / 未列网络 / 名字错)—— 与 server.ts 同为"默认拒绝"
assert.equal(isAllowedDialer(new Map(), 'ops', 'manager'), false)
assert.equal(isAllowedDialer(dialers(), 'ops', 'MANAGER'), false, 'hostId 大小写敏感(与 HOST_RE 口径一致)')
})
// ── T5 · 打洞成功路径(真 dgram + NAT 模拟) ────────────────────────────────────
test('T5 打洞成功路径:双向都成立才算直连(punchOk ≥ 1)', async () => {
const r = await runPunchPair({ aPeer: 'ops/w-1', bPeer: 'ops/w-2', deadlineMs: 2500 }, { sleep })
assert.equal(r.a.bidirectional, true, `A 侧应双向成立:${r.a.detail}`)
assert.equal(r.b.bidirectional, true, `B 侧应双向成立:${r.b.detail}`)
assert.equal(r.a.reason, 'ok')
assert.equal(r.b.reason, 'ok')
assert.equal(r.bidirectional, true)
assert.ok(r.a.recvLocal > 0 && r.b.recvLocal > 0, '两侧都必须真的收到包')
assert.equal(r.nat.a.forwardedIn > 0, true, 'NAT 侧记录:真的放行了入向包')
})
// ── T6 · 单向不算直连 ───────────────────────────────────────────────────────────
test('T6 单向 ⇒ 判死 one-way(⛔ 不许把单向当成功)', async () => {
const r = await runPunchPair({ aPeer: 'ops/w-1', bPeer: 'ops/w-2', deadlineMs: 1200, oneWay: 'a' }, { sleep })
assert.equal(r.bidirectional, false)
assert.equal(r.a.reason, 'one-way', `A 侧(只能出不能进)应判 one-way:${r.a.detail}`)
assert.equal(r.a.bidirectional, false)
assert.equal(r.b.reason, 'one-way', `B 侧(收到包但对端没收到)应判 one-way:${r.b.detail}`)
assert.equal(r.b.recvLocal > 0, true, 'B 的确收到了包 ⇒ 不能因为"有收包"就判成功')
assert.equal(r.b.peerSeen, false)
})
// ── T7 · 失败判死有界 ───────────────────────────────────────────────────────────
test('T7 对端不响应 ⇒ 窗内判死,且耗时**有界**(⛔ 不无限重试)', async () => {
const cd = new DirectCooldown(DEFAULT_DIRECT_COOLDOWN_MS)
const deadlineMs = 600
const r = await runPunchAttempt(
{
peer: 'ops/void',
selfPort: 0,
targets: [{ host: '127.0.0.1', port: await deadPort() }],
deadlineMs,
cooldown: cd,
bindHost: '127.0.0.1',
},
{ sleep },
)
assert.equal(r.reason, 'deadline')
assert.equal(r.ok, false)
assert.equal(r.recvLocal, 0)
assert.ok(r.sent > 1, '窗内应重发(单发一次赶不上对端同时发包)')
assert.ok(r.elapsedMs <= deadlineMs + 3 * 150, `耗时必须有界:${r.elapsedMs} ms`)
assert.equal(cd.snapshot().cooling.includes('ops/void'), true, '判死 ⇒ 该候选进冷却')
// 没有候选 ⇒ **不是**打洞失败 ⇒ ⛔ 不进冷却
const cd2 = new DirectCooldown(DEFAULT_DIRECT_COOLDOWN_MS)
const r2 = await runPunchAttempt({ peer: 'ops/none', selfPort: 0, targets: [], cooldown: cd2 }, { sleep })
assert.equal(r2.reason, 'no-address')
assert.equal(cd2.snapshot().cooling.length, 0)
})
// ── T8 · 冷却 ──────────────────────────────────────────────────────────────────
test('T8 冷却:判死后二次被挡且**不开 socket**;`ms = 0` 构造即抛', async () => {
const cd = new DirectCooldown(5_000)
let opened = 0
const port = await deadPort()
const mk = (targets) =>
runPunchAttempt(
{
peer: 'ops/void',
selfPort: 0,
targets,
deadlineMs: 300,
cooldown: cd,
bindHost: '127.0.0.1',
onSocketOpen: () => {
opened += 1
},
},
{ sleep },
)
const first = await mk([{ host: '127.0.0.1', port }])
assert.equal(first.reason, 'deadline')
assert.equal(opened, 1)
const second = await mk([{ host: '127.0.0.1', port }])
assert.equal(second.reason, 'cooldown')
assert.equal(opened, 1, '冷却命中的路径**一个 socket 都不许开**')
assert.ok(cd.snapshot().blocked >= 1)
// 🔴 0 = 重试风暴 ⇒ 构造期就炸(本线硬禁令)
assert.throws(() => new DirectCooldown(0), /必须 > 0/)
assert.throws(() => new DirectCooldown(-1), /必须 > 0/)
assert.throws(() => new DirectCooldown(Number.NaN), /必须 > 0/)
})
// ── T9/T10 · join 回读 + 提示 ─────────────────────────────────────────────────
test('T9 join 本机配置读回 direct = true,且 --direct 0 被尊重', async () => {
const tmp = mkdtempSync(join(tmpdir(), 'dshs-direct-t9-'))
const signer = generateAuthorityKey()
const issue = () => {
const doc = {
version: NODES_REGISTRY_VERSION,
network: 't9-net',
nonce: newInviteNonce(randomBytes),
issuedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 60_000).toISOString(),
}
return { doc, sig: signPayloadWith(signer.privateKeyPem, networkInvitePayload(doc)) }
}
const io = nodeJoinIo({
fss: { existsSync, readFileSync, writeFileSync, mkdirSync },
crypto: { generateNodeKey: () => generateNodeKey(), publicKeyOfPrivate: (pem) => publicKeyOfPrivate(pem) },
os: { hostname: () => 't9-host' },
fetchImpl: async () => ({ status: 599, body: 'n/a' }),
})
const run = (name, extra) =>
runJoin(
{
network: 't9-net',
hostId: name,
invite: issue(),
trustedSigners: [signer.publicKey],
nodeKeyFile: join(tmp, `${name}.key`),
localConfigFile: join(tmp, `${name}.json`),
outFile: join(tmp, `${name}-app.json`),
...extra,
},
io,
)
const a = await run('node-a', {})
assert.equal(a.ok, true)
const cfgA = JSON.parse(readFileSync(join(tmp, 'node-a.json'), 'utf8'))
assert.equal(cfgA.direct, true, '缺省必须写进本机配置 = 开(用户口径②)')
assert.equal(directFromNodeConfig(cfgA), true)
assert.match(a.steps.find((s) => s.step === 'local-config').detail, /直连\(打洞\)=开/)
const b = await run('node-b', { direct: false })
assert.equal(b.ok, true)
assert.equal(directFromNodeConfig(JSON.parse(readFileSync(join(tmp, 'node-b.json'), 'utf8'))), false)
// 管理面写回:**只改 direct 一个字段**,其余原样;缺失文件 ⇒ 具名失败(⛔ 不凭空创建)
const file = join(tmp, 'node-a.json')
const before = JSON.parse(readFileSync(file, 'utf8'))
const writer = {
exists: existsSync,
read: (p) => readFileSync(p, 'utf8'),
write: (p, t, mode) => writeFileSync(p, t, { mode }),
rename: renameSync,
}
const w = writeNodeConfigDirect(file, false, writer)
assert.equal(w.ok, true)
const after = JSON.parse(readFileSync(file, 'utf8'))
assert.equal(after.direct, false)
assert.deepEqual({ ...after, direct: true }, before, '⛔ 除 direct 外任何字段都不许被动到')
const miss = writeNodeConfigDirect(join(tmp, 'nope.json'), true, writer)
assert.equal(miss.ok, false)
assert.equal(miss.reason, 'node-config-missing')
// 形状坏 ⇒ 具名
writeFileSync(join(tmp, 'bad.json'), '[1,2,3]')
assert.throws(() => readNodeConfig(join(tmp, 'bad.json'), { exists: () => true, read: (p) => readFileSync(p, 'utf8') }), /不是 JSON 对象/)
})
test('T10 提示文案必须**可行动**:三段齐 + 含开关键与关值', () => {
const lines = directHintLines()
assert.equal(lines.length, 3)
const text = directHintText()
assert.ok(text.includes('同一张覆盖网') && text.includes('白名单'), '①段要写清"谁可能连进来"')
assert.ok(text.includes(DIRECT_ENV_KEY), '②段要写清"怎么关"(含开关键名)')
assert.ok(DIRECT_OFF_VALUES.some((v) => text.includes(v)), '②段要给出具体的关闭取值')
assert.ok(text.includes('不影响'), '③段要写清"关掉不影响什么"')
assert.ok(text.includes('中继'), '③段要写清回落路径')
// ⛔ 禁止只写"已启用直连"
assert.ok(!/^已启用直连$/.test(text.trim()))
// 编码侧的结构性防线:**只吃白名单字段** ⇒ 多给的任何字段(含凭据)都进不了载荷
const encoded = JSON.parse(encodeDirectMessage({ hostId: 'w-2', network: 'ops', addrs: [{ host: '10.0.0.9', port: 1 }], secret: 'x' }))
assert.deepEqual(Object.keys(encoded).sort(), ['addrs', 'hostId', 'kind', 'network', 'ts'])
assert.equal(encoded.secret, undefined, '⛔ 载荷里不可能夹带凭据字段(构造侧结构性排除)')
})