Files
dsh_shenxian/test/overlay-network.test.mjs
T

527 lines
24 KiB
JavaScript
Raw Normal View History

/**
* 覆盖网络 · **② 网抽象(P0-1)** 单测 —— `network_id` 是不是**结构性隔离**。
*
* ## 这个文件要回答的唯一问题
* 「不同网络的节点之间**不能互相到达**」这件事,是**结构上做不到**,还是"靠某条 ACL 记得拦"?
* —— 所以本文件的验收标准不是"配置写对了",而是:
* 1. **跨网拨号被 relay 拒**,且拒绝点在**服务端**(有日志、有计数);
* ⚠️ P0-3 起**对外没有专属拒码**:跨网一律回 `target-offline`(与"本网无此节点"逐字同形),
* 区分只留在服务端日志 —— 否则"它在别张网"本身就是一份**可探测的对端清单**。
* 2. **同名 hostId 在两张网里互不干扰**(会话表按 `<network>/<hostId>` 索引,不是扁平 `hostId`);
* 3. **旧形态照旧可用**(旧客户端不声明 `network`、旧 drop-in 的扁平白名单 ⇒ 全部落在 `ops`,
* 现网 47 / 106 一行配置不改也不受影响)—— "不破坏现网"与"真落地"必须同时成立。
*
* 运行:`node --test test/overlay-network.test.mjs`(Node ≥ 22;测的是 `lib/` 产物,先 `npm run build`)。
*
* @module test/overlay-network
*/
import assert from 'node:assert/strict'
import { createHmac, randomBytes } from 'node:crypto'
import { createServer as createTcpServer, connect } from 'node:net'
import { test } from 'node:test'
import {
MUX,
OPS_NETWORK,
RelayClient,
RelayServer,
assertNetworkId,
decodeMux,
describeDialers,
encodeJsonFrame,
logicalName,
normalizeDialers,
parseLogicalName,
} from '../lib/net/relay/index.js'
const BASE = 46000
const SPAN = 200
const PATH = '/dshs-relay'
const U_TEST = 'u:test-network'
/* ─────────── 小工具(与 relay.test.mjs 同款,本文件自足) ─────────── */
async function listenInRange(server, lo, hi) {
for (let p = lo; p < hi; p++) {
const ok = await new Promise((resolve) => {
const onErr = () => {
server.off('error', onErr)
resolve(false)
}
server.once('error', onErr)
server.listen(p, '127.0.0.1', () => {
server.off('error', onErr)
resolve(true)
})
})
if (ok) return server.address().port
}
throw new Error(`no free port in ${lo}..${hi}`)
}
async function waitFor(cond, ms = 5000) {
const t0 = Date.now()
while (Date.now() - t0 < ms) {
if (cond()) return true
await new Promise((r) => setTimeout(r, 20))
}
return cond()
}
/** 回显服务,但**带一个前缀** —— 用来分辨"这条流到底落在哪张网的那台机上"。 */
function taggedEcho(tag) {
const server = createTcpServer((s) => s.on('data', (c) => s.write(`${tag}:${c.toString('utf8')}`)))
return server
}
/** 拨号流往返:写进去、读回(前缀长度显式传入,避免"读到一半就认为对了")。 */
function dialRoundTrip(duplex, text, prefixLen = 0, ms = 5000) {
return new Promise((resolve, reject) => {
let got = ''
const timer = setTimeout(() => {
duplex.destroy()
reject(new Error(`dial roundTrip timeout (got ${got.length}/${text.length + prefixLen})`))
}, ms)
duplex.on('data', (chunk) => {
got += chunk.toString('utf8')
if (got.length >= text.length + prefixLen) {
clearTimeout(timer)
resolve(got)
}
})
duplex.on('error', (err) => {
clearTimeout(timer)
reject(err)
})
duplex.write(text)
})
}
/**
* 手工发一帧 `HELLO`(构造"正常 client 做不到"的输入:**不带** `network` 的旧客户端、非法 `network`)。
*
* `network === undefined` ⇒ 字段**根本不出现**(= 现网 47/106 上正在跑的旧客户端)。
*/
async function rawHello(wsUrl, hostId, secret, portsCsv, nonce, network) {
const ws = new WebSocket(wsUrl)
ws.binaryType = 'arraybuffer'
await new Promise((resolve, reject) => {
ws.addEventListener('open', resolve, { once: true })
ws.addEventListener('error', () => reject(new Error('ws open failed')), { once: true })
})
const ts = Date.now()
// ⚠️ 与 client.ts 逐字同源的 MAC 输入(**不含 network**)—— 这条"一字不改"本身就是被测对象之一:
// 旧客户端算出来的 MAC 必须仍被接受。
const mac = createHmac('sha256', Buffer.from(secret, 'hex')).update(`${hostId}|${ts}|${nonce}|${portsCsv}`).digest('hex')
const payload = { v: 1, hostId, ts, nonce, portsCsv, mac }
if (network !== undefined) payload.network = network
ws.send(encodeJsonFrame(MUX.HELLO, 0, payload))
const frame = await new Promise((resolve) => {
const timer = setTimeout(() => resolve(undefined), 3000)
ws.addEventListener('message', (ev) => {
clearTimeout(timer)
resolve(decodeMux(Buffer.from(ev.data)))
})
ws.addEventListener('close', () => {
clearTimeout(timer)
resolve(undefined)
})
})
return { ws, frame }
}
/** 在一个**已建立**的连接上手工发一帧、等回帧(用于构造"正常 client 不会做"的请求)。 */
async function rawSend(ws, frameBuf) {
const p = new Promise((resolve) => {
const timer = setTimeout(() => resolve(undefined), 3000)
ws.addEventListener('message', (ev) => {
clearTimeout(timer)
resolve(decodeMux(Buffer.from(ev.data)))
})
ws.addEventListener('close', () => {
clearTimeout(timer)
resolve(undefined)
})
})
ws.send(frameBuf)
return p
}
/* ─────────── U1:纯函数(逻辑名与白名单解析) ─────────── */
test('U1 逻辑名唯一入口:旧形态 / 新形态 / `u:<租户>` 里的冒号 / 非法值一律抛', () => {
// ① 规范形态
assert.equal(logicalName('u:5', 'w-1'), 'u:5/w-1')
assert.deepEqual(parseLogicalName('ops/manager'), { network: 'ops', hostId: 'manager' })
assert.deepEqual(parseLogicalName('u:5/w-106'), { network: 'u:5', hostId: 'w-106' })
// ② 兼容形态:`network:hostId`(运维习惯写法)
assert.deepEqual(parseLogicalName('ops:manager'), { network: 'ops', hostId: 'manager' })
// 🔴 必须**从右**切:`u:5` 里的冒号属于网络 id,从左切会得到 network='u'(不存在那张网 ⇒ 静默拒绝)
assert.deepEqual(parseLogicalName('u:5:manager'), { network: 'u:5', hostId: 'manager' })
assert.deepEqual(parseLogicalName('u:test-network:pc-1'), { network: 'u:test-network', hostId: 'pc-1' })
// ③ 旧形态(R5 时代的扁平 hostId)⇒ 落在运维网,**旧配置照旧可用**
assert.deepEqual(parseLogicalName('manager'), { network: OPS_NETWORK, hostId: 'manager' })
assert.deepEqual(parseLogicalName('w-106'), { network: OPS_NETWORK, hostId: 'w-106' })
// ④ 非法 ⇒ **抛**(配置错就炸,不静默变成"谁也没匹配上")
assert.throws(() => parseLogicalName('u:5'), /网络段/)
assert.throws(() => parseLogicalName('bad net:manager'), /网络段/)
assert.throws(() => parseLogicalName('ops/'), /逻辑名/)
assert.throws(() => assertNetworkId('Ops'), /非法/)
assert.throws(() => assertNetworkId(''), /非法/)
// ⑤ 白名单归一化:扁平 `Set` = 全在 ops;`Map` 按网络分桶
assert.deepEqual([...normalizeDialers(new Set(['manager'])).get(OPS_NETWORK)], ['manager'])
const m = normalizeDialers(new Map([[U_TEST, new Set(['d1'])]]))
assert.equal(m.has(OPS_NETWORK), false, '没列到的网络不该凭空出现')
assert.deepEqual([...m.get(U_TEST)], ['d1'])
// 混写(桶里再带前缀)不会静默失配;非法网络段在装载时就炸
const mixed = normalizeDialers(new Set(['ops:manager', 'u:5:d1']))
assert.deepEqual(describeDialers(mixed), ['manager', 'u:5/d1'], 'ops 省略前缀、其它网写全逻辑名')
assert.throws(() => normalizeDialers(new Set(['BAD net:d1'])), /网络段/)
// 分桶形态:桶内的**裸 hostId 属于本桶那张网**(不是 ops)—— 这一条错了会变成"配对了却静默拒绝"
assert.deepEqual([...normalizeDialers(new Map([['u:5', new Set(['d1'])]])).get('u:5')], ['d1'])
// 桶内带了前缀就必须与本桶一致,否则说明配置自相矛盾 ⇒ 装载时就炸
assert.throws(() => normalizeDialers(new Map([['u:5', new Set(['ops:d1'])]])), /自相矛盾/)
// ⑥ 客户端侧同一条口径:非法 networkId ⇒ 构造时抛(不静默回落 ops)
assert.throws(
() => new RelayClient({ url: 'ws://127.0.0.1:1' + PATH, hostId: 'x', secret: randomBytes(32).toString('hex'), ports: [BASE], networkId: 'BAD' }),
/networkId/,
)
})
/* ─────────── U2:现网不退化(旧客户端 + 旧白名单写法) ─────────── */
test('U2 旧客户端(不声明 network)+ 旧扁平白名单 ⇒ 一切照旧,全部落在 ops', async (t) => {
const wSecret = randomBytes(32).toString('hex')
const w47Secret = randomBytes(32).toString('hex')
const mSecret = randomBytes(32).toString('hex')
const echo = taggedEcho('ops')
const servePort = await listenInRange(echo, BASE + 10, BASE + SPAN - 1)
const legacyPort = BASE + 12
const srv = new RelayServer({
port: 0,
keys: new Map([
['w-106', wSecret],
['w-47', w47Secret],
['manager', mSecret],
]),
instancePortBase: BASE,
instancePortSpan: SPAN,
// 🔴 旧写法:扁平 `Set`(= 现网 `DSHS_RELAY_DIALERS="manager"` 的形态)
dialers: new Set(['manager']),
log: () => {},
})
await srv.start()
const url = `ws://127.0.0.1:${srv.boundPort}${PATH}`
t.after(async () => {
echo.close()
await srv.stop()
})
// ① **旧客户端握手**:HELLO 里根本没有 network 字段(= 现网 47/106 上正在跑的那一版)
const { ws: rawWs, frame: ack } = await rawHello(url, 'w-106', wSecret, String(legacyPort), randomBytes(16).toString('hex'))
t.after(() => rawWs.close())
assert.ok(ack !== undefined, '旧客户端必须能注册(否则这次改动就是硬断)')
assert.equal(ack.type, MUX.HELLO_ACK, '旧客户端应拿到 HELLO_ACK')
const parsed = JSON.parse(ack.payload.toString('utf8'))
assert.equal(parsed.network, OPS_NETWORK, 'HELLO_ACK 应回显服务端认定的网 = ops')
assert.equal(parsed.name, logicalName(OPS_NETWORK, 'w-106'))
assert.ok(await waitFor(() => srv.isOnline('w-106')), '默认网络(ops)里应看到它')
assert.ok(
await waitFor(() => srv.localPortOf('w-106', legacyPort) !== undefined),
'旧客户端照样拿到回环落点(R1–R4 的行为不变)',
)
// ② 不传 `networkId` 的真 client = ops(默认值即现网事实);旧扁平白名单照旧拨得动
const worker = new RelayClient({ url, hostId: 'w-47', secret: w47Secret, ports: [servePort], log: () => {} })
const dialer = new RelayClient({ url, hostId: 'manager', secret: mSecret, ports: [], dialer: true, log: () => {} })
worker.start()
dialer.start()
t.after(() => {
worker.stop()
dialer.stop()
})
assert.ok(await waitFor(() => worker.status().state === 'up'), 'worker 未注册')
assert.equal(worker.status().network, OPS_NETWORK, '不声明网络 ⇒ 默认 ops(不是空、不是 undefined)')
assert.ok(await waitFor(() => dialer.status().state === 'up'), '拨号方未注册')
assert.deepEqual(srv.status().dialers, ['manager'], '/status 的 dialers 仍按旧写法展示(不破坏既有消费者)')
const duplex = await dialer.openStream('w-47', servePort)
assert.equal(await dialRoundTrip(duplex, 'legacy-ok', 'ops:'.length), 'ops:legacy-ok', '字节要真的过去')
duplex.destroy()
})
/* ─────────── U3:跨网隔离是**结构性**的(拒绝点在 relay) ─────────── */
test('U3 跨网隔离:u:test-network 的合法拨号方拨 ops/w-106 ⇒ relay 拒,且**不泄露**目标在哪张网', async (t) => {
const wSecret = randomBytes(32).toString('hex')
const opsSecret = randomBytes(32).toString('hex')
const foreignSecret = randomBytes(32).toString('hex')
const logs = []
const echo = taggedEcho('ops')
const workerPort = await listenInRange(echo, BASE + 20, BASE + SPAN - 1)
const srv = new RelayServer({
port: 0,
keys: new Map([
['w-106', wSecret],
['manager', opsSecret],
// 序③:`dt` 真正属于 U_TEST(密钥表说了算)⇒ 它能进自己的网,然后在 **DIAL 那一步**
// 被跨网判据挡住 —— 这才是本用例要测的那道门,而不是『压根没登记』那道。
[logicalName(U_TEST, 'dt'), foreignSecret],
]),
instancePortBase: BASE,
instancePortSpan: SPAN,
/**
* 两张网**各自都有合法的拨号方**(不是"没配白名单"那种低级情形)——
* 这样才证明隔离来自**网络维度本身**,而不是来自"忘了加白名单"。
*/
dialers: new Map([
[OPS_NETWORK, new Set(['manager'])],
[U_TEST, new Set(['dt'])],
]),
log: (line) => logs.push(line),
})
await srv.start()
const url = `ws://127.0.0.1:${srv.boundPort}${PATH}`
const worker = new RelayClient({ url, hostId: 'w-106', secret: wSecret, ports: [workerPort], log: () => {} })
const foreign = new RelayClient({
url,
hostId: 'dt',
secret: foreignSecret,
ports: [],
dialer: true,
networkId: U_TEST,
log: (line) => logs.push(`[client] ${line}`),
})
worker.start()
foreign.start()
t.after(async () => {
worker.stop()
foreign.stop()
echo.close()
await srv.stop()
})
const opsUp = await waitFor(() => srv.isOnline('w-106'))
assert.ok(opsUp, 'ops 侧 worker 未注册')
const foreignUp = await waitFor(() => srv.isOnline('dt', U_TEST))
assert.ok(foreignUp, `别网拨号方未注册;最近日志:${logs.slice(-10).join(' | ')}`)
// 网维度视图必须可断言(Step 3「看不到」那一半的落地基础)
const nets = srv.status().networks
assert.deepEqual(
nets.find((n) => n.network === OPS_NETWORK)?.sessions,
['w-106'],
'ops 网里应只有 w-106',
)
assert.deepEqual(nets.find((n) => n.network === U_TEST)?.sessions, ['dt'], '别张网里应只有 dt')
// 🔴 核心判据 ①:**拨不动**
const before = srv.status().counters.refused
let foreignMsg = ''
await assert.rejects(
() => foreign.openStream('w-106', workerPort),
(err) => {
foreignMsg = err instanceof Error ? err.message : String(err)
return true
},
'跨网必须被拒',
)
// 🔴 核心判据 ②:**看不到** —— 对外与"本网没有这个节点"逐字同形(P0-3 · D4 不下发对端清单)。
// 若这里回 `dialer-not-in-network` / `targetNetwork`,拨号方拿一串猜测的 hostId 打过来
// 就能把别张网的成员**枚举出来**(判据「看不到」当场失效)。
assert.match(foreignMsg, /target-offline/, `对外必须是 target-offline,实测:${foreignMsg}`)
assert.equal(foreignMsg.includes(U_TEST), false, `响应里不得出现任何网络名,实测:${foreignMsg}`)
assert.equal(
foreignMsg.includes('dialer-not-in-network'),
false,
`对外不得有"跨网"专属拒码,实测:${foreignMsg}`,
)
// 但**服务端**必须留得下区分(否则运维分不清"配错网"与"节点离线")
assert.ok(srv.status().counters.refused > before, '拒绝必须在 relay 侧计数(本机就得留痕)')
assert.ok(
logs.some((l) => l.includes('dialer-not-in-network')),
`relay 日志必须记下跨网拒绝(实测日志:${logs.filter((l) => l.includes('DIAL')).join(' | ')})`,
)
// 目标侧没有任何流被建立(不是"建了再断")
assert.equal(
srv.status().endpoints.find((e) => e.hostId === 'w-106' && e.network === OPS_NETWORK && e.port === workerPort)?.streams,
0,
'被拒的跨网拨号不得在目标侧留下流',
)
assert.equal(foreign.status().dialStreams, 0, '被拒后拨号方也不该留悬挂流')
// 反证:**同网**照旧全通 ⇒ 隔离挡的是"跨网",不是"拨号"本身
const ops = new RelayClient({ url, hostId: 'manager', secret: opsSecret, ports: [], dialer: true, log: () => {} })
ops.start()
t.after(() => ops.stop())
assert.ok(await waitFor(() => ops.status().state === 'up'), 'ops 拨号方未注册')
const duplex = await ops.openStream('w-106', workerPort)
assert.equal(await dialRoundTrip(duplex, 'same-net', 'ops:'.length), 'ops:same-net')
duplex.destroy()
})
/* ─────────── U4 / U5:未列白名单的网 & 跨网同名 hostId ─────────── */
test('U4 没列进白名单的网:连"拨号方"这个身份都拿不到(no-ports,默认拒绝)', async (t) => {
const s1 = randomBytes(32).toString('hex')
const srv = new RelayServer({
port: 0,
// 序③:`dt` 登记在 U_TEST ⇒ 成员资格这一关**过得去**,于是拒绝来自**白名单**那一道门
//(`no-ports`)。不这么登记的话,成员资格会先一步拒,本用例就测不到它原本要测的东西。
keys: new Map([[logicalName(U_TEST, 'dt'), s1]]),
instancePortBase: BASE,
instancePortSpan: SPAN,
dialers: new Map([[OPS_NETWORK, new Set(['manager'])]]), // 只有 ops 有拨号方
log: () => {},
})
await srv.start()
const url = `ws://127.0.0.1:${srv.boundPort}${PATH}`
t.after(async () => {
await srv.stop()
})
/**
* R1 的**默认拒绝**姿态决定了一件好事:拨号方的形态是"**端口表为空**",而空端口表只在
* 该 hostId 命中**本网**白名单时才被接受 ⇒ **白名单外的网络连"拨号方身份"都拿不到**
* (比"拿到了身份但拨不动"更早一道门)。这比 U3 那道门更靠前,两条都要能回归。
*/
const before = srv.status().counters.authFailed
const { ws, frame } = await rawHello(url, 'dt', s1, '', randomBytes(16).toString('hex'), U_TEST)
t.after(() => ws.close())
assert.ok(frame !== undefined, '必须得到结构化拒绝')
assert.equal(frame.type, MUX.HELLO_ERR)
assert.equal(JSON.parse(frame.payload.toString('utf8')).reason, 'no-ports', '白名单外的网不该拿到拨号方身份')
assert.ok(srv.status().counters.authFailed > before)
assert.equal(srv.status().sessions.length, 0)
// 对照:同一个 hostId 只要落进 `ops` 桶(且声明 ops),立刻拿到 `dialer: true`
// ⇒ 说明拒绝来自**网络归属**,不是来自"这个 hostId 本身没被信任"。
const srv2 = new RelayServer({
port: 0,
keys: new Map([
['dt', s1],
['w2', s1],
]),
instancePortBase: BASE,
instancePortSpan: SPAN,
dialers: new Map([[OPS_NETWORK, new Set(['dt'])]]),
log: () => {},
})
await srv2.start()
t.after(async () => {
await srv2.stop()
})
const ok = await rawHello(`ws://127.0.0.1:${srv2.boundPort}${PATH}`, 'dt', s1, '', randomBytes(16).toString('hex'), OPS_NETWORK)
t.after(() => ok.ws.close())
assert.equal(ok.frame?.type, MUX.HELLO_ACK)
assert.equal(JSON.parse(ok.frame.payload.toString('utf8')).dialer, true)
/**
* ② `not-a-dialer` 这条门(R5 就有的默认拒绝)不能被这次改动改坏:
* 一个**已注册的普通 worker**(有端口、不在拨号桶里)手工发 `DIAL` ⇒ relay 必须回拒。
*/
const wPort = BASE + 95
const w2 = await rawHello(`ws://127.0.0.1:${srv2.boundPort}${PATH}`, 'w2', s1, String(wPort), randomBytes(16).toString('hex'), OPS_NETWORK)
t.after(() => w2.ws.close())
assert.equal(w2.frame?.type, MUX.HELLO_ACK, 'worker 形态(声明端口)应能注册')
const ack = await rawSend(w2.ws, encodeJsonFrame(MUX.DIAL, 1, { target: 'dt', port: wPort }))
assert.equal(ack?.type, MUX.DIAL_ACK, 'DIAL 必须被应答(不能静默)')
const ackBody = JSON.parse(ack.payload.toString('utf8'))
assert.equal(ackBody.ok, false)
assert.equal(ackBody.error, 'not-a-dialer', '不在本网拨号桶里的会话不得发起 DIAL')
})
test('U5 跨网同名 hostId 互不干扰:各拨各的(会话表按逻辑名索引)', async (t) => {
const wSecret = randomBytes(32).toString('hex')
const dOps = randomBytes(32).toString('hex')
const dUser = randomBytes(32).toString('hex')
const opsEcho = taggedEcho('ops')
const userEcho = taggedEcho('user')
const opsPort = await listenInRange(opsEcho, BASE + 40, BASE + 60)
const userPort = await listenInRange(userEcho, BASE + 61, BASE + 80)
assert.notEqual(opsPort, userPort)
const srv = new RelayServer({
port: 0,
// 序③:键 = **逻辑名** ⇒ 同一台机器名可以**同时**出现在两张网里,各自是一条独立登记项。
//(用裸 hostId 当键做不到这件事:`w-1` 只能属于一张网 ⇒ 第二张网就注册不进来了。)
keys: new Map([
[logicalName(OPS_NETWORK, 'w-1'), wSecret],
[logicalName('u:x', 'w-1'), wSecret],
[logicalName(OPS_NETWORK, 'm-ops'), dOps],
[logicalName('u:x', 'd-user'), dUser],
]),
instancePortBase: BASE,
instancePortSpan: SPAN,
dialers: new Map([
[OPS_NETWORK, new Set(['m-ops'])],
['u:x', new Set(['d-user'])],
]),
log: () => {},
})
await srv.start()
const url = `ws://127.0.0.1:${srv.boundPort}${PATH}`
const wOps = new RelayClient({ url, hostId: 'w-1', secret: wSecret, ports: [opsPort], networkId: OPS_NETWORK, log: () => {} })
const wUser = new RelayClient({ url, hostId: 'w-1', secret: wSecret, ports: [userPort], networkId: 'u:x', log: () => {} })
const dOpsC = new RelayClient({ url, hostId: 'm-ops', secret: dOps, ports: [], dialer: true, log: () => {} })
const dUserC = new RelayClient({ url, hostId: 'd-user', secret: dUser, ports: [], dialer: true, networkId: 'u:x', log: () => {} })
for (const c of [wOps, wUser, dOpsC, dUserC]) c.start()
t.after(async () => {
for (const c of [wOps, wUser, dOpsC, dUserC]) c.stop()
opsEcho.close()
userEcho.close()
await srv.stop()
})
assert.ok(await waitFor(() => srv.isOnline('w-1', OPS_NETWORK)), 'ops 侧 w-1 未注册')
assert.ok(await waitFor(() => srv.isOnline('w-1', 'u:x')), 'u:x 侧 w-1 未注册')
const w1Names = srv.status()
.sessions.filter((s) => s.hostId === 'w-1')
.map((s) => s.name)
.sort()
assert.deepEqual(w1Names, ['ops/w-1', 'u:x/w-1'], '两个同名节点必须各自在册(扁平命名空间会只剩一个)')
// 两张网的拨号方各拨**自己网里**的 w-1 ⇒ 各自落到自己那张网的回显服务(前缀不同 ⇒ 不可能看错)
const dOpsStream = await dOpsC.openStream('w-1', opsPort)
assert.equal(await dialRoundTrip(dOpsStream, 'A', 'ops:'.length), 'ops:A')
dOpsStream.destroy()
const dUserStream = await dUserC.openStream('w-1', userPort)
assert.equal(await dialRoundTrip(dUserStream, 'B', 'user:'.length), 'user:B')
dUserStream.destroy()
})
/* ─────────── U6:非法 network ⇒ 失败关闭 ─────────── */
test('U6 非法 network 声明 ⇒ bad-network 失败关闭(不建会话、不静默当 ops)', async (t) => {
const secret = randomBytes(32).toString('hex')
const srv = new RelayServer({
port: 0,
keys: new Map([['w-x', secret]]),
instancePortBase: BASE,
instancePortSpan: SPAN,
log: () => {},
})
await srv.start()
const url = `ws://127.0.0.1:${srv.boundPort}${PATH}`
t.after(async () => {
await srv.stop()
})
const before = srv.status().counters.authFailed
const { ws, frame } = await rawHello(url, 'w-x', secret, String(BASE + 90), randomBytes(16).toString('hex'), 'bad net!')
t.after(() => ws.close())
assert.ok(frame !== undefined, '非法网必须得到**结构化拒绝**,而不是静默断开')
assert.equal(frame.type, MUX.HELLO_ERR, '应回 HELLO_ERR')
assert.equal(JSON.parse(frame.payload.toString('utf8')).reason, 'bad-network')
assert.ok(srv.status().counters.authFailed > before, '拒绝要计数')
assert.equal(srv.status().sessions.length, 0, '非法网不得建会话(更不得落到 ops)')
assert.equal(srv.localPortOf('w-x', BASE + 90), undefined, '被拒的节点不得留下任何回环监听')
})
/* ─────────── 说明:本文件不覆盖的部分 ─────────── */
test('U7 本文件的范围声明(Step 1 只做 P0-1,别在这里顺手做后面两步)', () => {
// Step 2(引导三级链 / `/dshs-overlay/bootstrap`)与 Step 3(名字解析对外接口)见后续步骤,
// ⛔ 不要在本步追加 —— 每步单独可回滚(交接单 §4)。
assert.ok(true)
})