把散落在代码里的真实部署值统一收进 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 未被误建)。
528 lines
24 KiB
JavaScript
528 lines
24 KiB
JavaScript
/**
|
||
* 覆盖网络 · **② 网抽象(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-2'), { network: 'u:5', hostId: 'w-2' })
|
||
|
||
// ② 兼容形态:`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-2'), { network: OPS_NETWORK, hostId: 'w-2' })
|
||
|
||
// ④ 非法 ⇒ **抛**(配置错就炸,不静默变成"谁也没匹配上")
|
||
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-2', wSecret],
|
||
['w-1', 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-2', 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-2'))
|
||
assert.ok(await waitFor(() => srv.isOnline('w-2')), '默认网络(ops)里应看到它')
|
||
assert.ok(
|
||
await waitFor(() => srv.localPortOf('w-2', legacyPort) !== undefined),
|
||
'旧客户端照样拿到回环落点(R1–R4 的行为不变)',
|
||
)
|
||
|
||
// ② 不传 `networkId` 的真 client = ops(默认值即现网事实);旧扁平白名单照旧拨得动
|
||
const worker = new RelayClient({ url, hostId: 'w-1', 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-1', servePort)
|
||
assert.equal(await dialRoundTrip(duplex, 'legacy-ok', 'ops:'.length), 'ops:legacy-ok', '字节要真的过去')
|
||
duplex.destroy()
|
||
})
|
||
|
||
/* ─────────── U3:跨网隔离是**结构性**的(拒绝点在 relay) ─────────── */
|
||
|
||
test('U3 跨网隔离:u:test-network 的合法拨号方拨 ops/w-2 ⇒ 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-2', 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-2', 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-2'))
|
||
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-2'],
|
||
'ops 网里应只有 w-2',
|
||
)
|
||
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-2', 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-2' && 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-2', 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)
|
||
})
|