/** * 覆盖网络 · **② P0-3 名字解析与授权** 单测 —— 逻辑名是不是"唯一入口"、跨网是不是**真拒绝**。 * * ## 这个文件要回答的两个问题(= 交接单 §4 Step 3 的两条 + §5 判据 3) * 1. **`place` / `resolve` 的唯一入口收不收逻辑名** `/`? * —— 只收裸 `hostId` 时,网络维度得由**每个调用方**自己补;两张网各有一台 `w-1` 就会**串网**。 * ⛔ 判据不是"代码里写了逻辑名",而是**拿两张同名 host 实测:各解析各的,谁也不覆盖谁**。 * 2. **跨网访问在 relay / 控制面被拒**,且**不泄露**目标在哪张网? * —— 判据:`u:A` 的节点**看不到、也到不了** `u:B` 的节点;`ops` 对用户网**默认不可见**。 * "看不到" = 对"在别张网"与"根本不存在"的响应**逐字相同**(拿一串猜测的 hostId 打过来 * 也枚举不出别张网的成员)。 * * 运行:`node --test test/overlay-auth.test.mjs`(Node ≥ 22;测的是 `lib/` 产物,先 `npm run build`)。 * * @module test/overlay-auth */ import assert from 'node:assert/strict' import { randomBytes } from 'node:crypto' import { createServer as createTcpServer, connect } from 'node:net' import { test } from 'node:test' import { RelayClient } from '../lib/net/relay/client.js' import { RelayDialer } from '../lib/net/relay/dialer.js' import { RelayServer } from '../lib/net/relay/server.js' import { OPS_NETWORK, assertSameNetwork, logicalName, parseLogicalName } from '../lib/net/relay/network.js' import { agentBaseUrl, agentBaseUrlOf, parseReachability, VIA_LOCAL, VIA_RELAY } from '../lib/net/reachability.js' import { LocalRendezvous, ManagerSshRendezvous, RendezvousRegistry } from '../lib/net/rendezvous.js' const BASE = 47000 const SPAN = 200 const PATH = '/dshs-relay' const U_A = 'u:alpha' const U_B = 'u:beta' /* ─────────────────────────── 工具 ─────────────────────────── */ /** * 在 `[lo, hi)` 里挑一个空闲口,**返回端口号**(不是 server 对象 —— 传错了会变成 * `ports: [object Object]`,服务端只回一个 `bad-port`,看着像"网络不通")。 */ 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(fn, ms = 3000) { const until = Date.now() + ms while (Date.now() < until) { if (fn()) return true await new Promise((r) => setTimeout(r, 20)) } return false } /** * 回显服务:收到的每个 chunk 都回 `:<原字节>`。 * ⚠️ 与 `overlay-network.test.mjs` 同款(两个文件口径一致,免得"同一条判据两份行为")。 */ function taggedEcho(tag) { return createTcpServer((s) => s.on('data', (c) => s.write(`${tag}:${c.toString('utf8')}`))) } /** 拨号流往返:写进去、读回(前缀长度显式传入,避免"读到一半就认为对了")。 */ 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) }) } /* ─────────── A1:同网断言(控制面侧那道门的公共件) ─────────── */ test('A1 assertSameNetwork:同网静默通过,跨网**抛**且点名两边是哪张网', () => { assert.doesNotThrow(() => assertSameNetwork(OPS_NETWORK, OPS_NETWORK)) assert.doesNotThrow(() => assertSameNetwork(U_A, U_A)) // 抛而不是回 false:回 false 的写法会让"跨网拒绝"在日志里什么都不留(= 静默失效) assert.throws(() => assertSameNetwork(U_A, U_B), /跨网/) assert.throws(() => assertSameNetwork(U_A, U_B), new RegExp(U_A)) assert.throws(() => assertSameNetwork(U_A, U_B), new RegExp(U_B)) }) /* ─────────── A2:place 的唯一入口收逻辑名 ─────────── */ test('A2 parseReachability 的第一个参数是**逻辑名**:网络段由它切,调用方不许拼', () => { const r = parseReachability(logicalName(U_A, 'w-1'), 'http://127.0.0.1:19000', VIA_RELAY) assert.equal(r.hostId, 'w-1', 'hostId 必须是**裸**的(网络另存一栏,两者分开表达)') assert.equal(r.networkId, U_A) assert.equal(agentBaseUrl(r), 'http://127.0.0.1:19000', '取址与 S0/S2 逐字一致(网络维度不进地址)') // 裸 hostId 仍兼容 ⇒ 落 ops(过渡期:现网所有调用方一个字都不用改) assert.equal(parseReachability('w-1', 'http://127.0.0.1:19100', VIA_LOCAL).networkId, OPS_NETWORK) // 非法网络段 ⇒ **抛**(配错别伪装成"这张网里没有它") assert.throws(() => parseReachability('UPPER/w-1', 'http://x:1', VIA_LOCAL), /网络段/) }) /* ─────────── A3:resolve 的唯一入口收逻辑名,两张同名 host 不互相覆盖 ─────────── */ test('A3 两张网各有一台同名 w-1 ⇒ 解析各查各的(键是逻辑名,不是裸 hostId)', async () => { const table = new Map([ [logicalName(OPS_NETWORK, 'w-1'), '127.0.0.1:19001'], [logicalName(U_A, 'w-1'), '127.0.0.1:19002'], ]) const reg = new RendezvousRegistry([ new LocalRendezvous((name) => table.get(name)), new ManagerSshRendezvous({ target: 't', addressOf: (name) => table.get(name) }), ]) for (const impl of [reg.get(VIA_LOCAL), reg.get('manager-ssh')]) { const ops = await impl.resolve(logicalName(OPS_NETWORK, 'w-1')) const alpha = await impl.resolve(logicalName(U_A, 'w-1')) assert.equal(ops.networkId, OPS_NETWORK) assert.equal(ops.address, '127.0.0.1:19001', 'ops 的 w-1 拿到的是 ops 的落点') assert.equal(alpha.networkId, U_A) assert.equal(alpha.address, '127.0.0.1:19002', `${impl.id}:u:alpha 的 w-1 拿到的是 u:alpha 的落点(没被 ops 覆盖)`) assert.notEqual(ops.address, alpha.address, '同名 host 分属两张网 ⇒ 落点必须不同') } // 查表键是逻辑名 —— 这条断言看着废话,但正是"只按裸 hostId 建键"会炸的地方 assert.equal(await reg.get(VIA_LOCAL).resolve('w-1'), undefined, '裸 hostId 不该命中任何逻辑名条目') }) /* ─────────── A4:via=relay 时禁止回落到 endpoint ─────────── */ test('A4 via=relay 且解析不出落点 ⇒ **抛**(不许回落到 endpoint = relay 落点)', () => { const relayHost = { hostId: 'w-2', agentUrl: 'http://127.0.0.1:19000', via: VIA_RELAY } assert.throws(() => agentBaseUrlOf(relayHost), /拒绝回落到 endpoint/) // 一旦解析成功 ⇒ 照常取址,且**优先**于 endpoint const ok = { ...relayHost, reachability: { hostId: 'w-2', networkId: OPS_NETWORK, via: VIA_RELAY, address: '127.0.0.1:42067', scheme: 'http' }, } assert.equal(agentBaseUrlOf(ok), 'http://127.0.0.1:42067') // 非 relay 语义(同机直连 / ssh 隧道)照旧回落 endpoint —— 这条不能被误伤 assert.equal(agentBaseUrlOf({ hostId: 'w-1', agentUrl: 'http://127.0.0.1:19100', via: VIA_LOCAL }), 'http://127.0.0.1:19100') assert.equal(agentBaseUrlOf({ hostId: 'w-2', agentUrl: 'http://127.0.0.1:19000' }), 'http://127.0.0.1:19000') }) /* ─────────── A5:控制面侧的跨网门(RelayDialer 口池) ─────────── */ test('A5 RelayDialer:落点口**真的能拨通**(DIAL target 必须是裸 hostId)+ 跨网拒 + 不占槽位', async (t) => { const wSecret = randomBytes(32).toString('hex') const mSecret = randomBytes(32).toString('hex') const logs = [] const echo = taggedEcho('ops') const echoPort = await listenInRange(echo, BASE + 40, BASE + SPAN - 1) const srv = new RelayServer({ port: 0, keys: new Map([ ['wx', wSecret], ['manager', mSecret], ]), instancePortBase: BASE, instancePortSpan: SPAN, dialers: new Map([[OPS_NETWORK, new Set(['manager'])]]), log: (line) => logs.push(line), }) await srv.start() const url = `ws://127.0.0.1:${srv.boundPort}${PATH}` const worker = new RelayClient({ url, hostId: 'wx', secret: wSecret, ports: [echoPort], networkId: OPS_NETWORK, log: () => {}, }) const mClient = new RelayClient({ url, hostId: 'manager', secret: mSecret, ports: [], dialer: true, networkId: OPS_NETWORK, log: (line) => logs.push(`[m] ${line}`), }) assert.equal(mClient.networkId, OPS_NETWORK, '拨号通道声明的网要从客户端读(不另配一份)') const dialer = new RelayDialer({ client: mClient, portBase: BASE + 60, portSpan: 8, poolSize: 4, log: (l) => logs.push(l) }) worker.start() mClient.start() // ⚠️ **必须 await**:口池绑完之前 `localPortFor()` 恒返回 undefined(失败关闭)—— // 少了这一句,本条会以"同网拿不到落点"的样子失败,看着像跨网判据炸了,其实是没绑池。 await dialer.start() t.after(async () => { worker.stop() mClient.stop() dialer.close() await echo.close() await srv.stop() }) assert.ok(await waitFor(() => srv.isOnline('wx', OPS_NETWORK)), `worker 未注册:${logs.slice(-6).join(' | ')}`) assert.ok(await waitFor(() => srv.isOnline('manager', OPS_NETWORK)), 'manager 未注册') const name = logicalName(OPS_NETWORK, 'wx') const local = dialer.localPortFor(name, echoPort) assert.equal(typeof local, 'number', '同网必须给落点') /** * 🔴 **本条的承重判据**:真连一次落点口 ⇒ 触发 `onConn` ⇒ 走一次**真 DIAL**。 * * 为什么不能只测 `localPortFor` 的返回值:键从 P0-3 起是**逻辑名**,而 `DIAL.target` 要的是 * **裸 hostId** —— 漏剥网络段时服务端按 `ops/ops/wx` 找会话 ⇒ 回 `target-offline` * (看着像"节点离线",节点其实好好在册)。这个 bug **只测返回值是抓不到的** * (2026-09-17 线上实测踩过一次:`refused: 8 / streamsOpened: 0`)。 */ const sock = connect(local, '127.0.0.1') const got = await new Promise((resolve, reject) => { let acc = '' const timer = setTimeout(() => { sock.destroy() reject(new Error(`落点口未拨通,最近日志:${logs.slice(-6).join(' | ')}`)) }, 3000) sock.on('data', (b) => { acc += b.toString('utf8') if (acc.length >= 'ops:ping'.length) { clearTimeout(timer) resolve(acc) } }) sock.on('error', (e) => { clearTimeout(timer) reject(e) }) sock.on('connect', () => sock.write('ping')) }) assert.equal(got, 'ops:ping', '落点口必须真的把字节送到 worker 的本地端口(⇒ DIAL target 是裸 hostId)') sock.destroy() // 跨网:**回 undefined**(失败关闭),不是"给个口让它去撞" assert.equal(dialer.localPortFor(logicalName(U_A, 'wx'), echoPort), undefined) assert.ok( logs.some((l) => l.includes('跨网拒绝') && l.includes(U_A)), `跨网拒绝必须留日志(点名两边是哪张网),实测:${logs.join(' | ')}`, ) // 不占槽位:同网再取还是**同一个**口(池大小为 4,若被跨网请求污染,这里会变成新口) assert.equal(dialer.localPortFor(name, echoPort), local) }) /* ─────────── A6:合体判据 —— 看不到 + 到不了(真 server / 真拨号) ─────────── */ test('A6 跨网:到不了,且**分辨不出**"在别张网"与"根本不存在"(逐字相同的响应)', async (t) => { const secrets = { alpha: randomBytes(32).toString('hex'), beta: randomBytes(32).toString('hex'), alphaDialer: randomBytes(32).toString('hex'), betaDialer: randomBytes(32).toString('hex'), opsManager: randomBytes(32).toString('hex'), } const logs = [] const echoA = taggedEcho('alpha') const echoB = taggedEcho('beta') const portA = await listenInRange(echoA, BASE + 20, BASE + SPAN - 1) const portB = await listenInRange(echoB, BASE + 21, BASE + SPAN - 1) const srv = new RelayServer({ port: 0, /** * ⚠️ **序③ 起:密钥表是成员资格的唯一判据** —— 键用**逻辑名** `<网>/`(`keys.ts`), * 所以"这台 host 属于哪张网"由**配置**决定,而不是由 HELLO 的声明决定。 * ⇒ 本测试里每台 host 都按**它真正所在的网**登记;声明别的网会被 `network-mismatch` * 挡在 HELLO 那一步(比 DIAL 那道门更早,也更省一次会话)。 * 「两张网各有一台同名 `w-1` 且互不覆盖」由 **A3** 在解析入口上判(唯一入口), * 由 **overlay-network.U5** 在真连接上判(那里两张网各有自己的 `w-1` 登记项)。 */ keys: new Map([ [logicalName(U_A, 'aw1'), secrets.alpha], [logicalName(U_B, 'bw1'), secrets.beta], [logicalName(U_A, 'ad'), secrets.alphaDialer], [logicalName(U_B, 'bd'), secrets.betaDialer], [logicalName(OPS_NETWORK, 'manager'), secrets.opsManager], ]), instancePortBase: BASE, instancePortSpan: SPAN, /** 三张网**各自都有合法拨号方** ⇒ 拒绝只能来自网络维度本身,不是"忘了配白名单"。 */ dialers: new Map([ [OPS_NETWORK, new Set(['manager'])], [U_A, new Set(['ad'])], [U_B, new Set(['bd'])], ]), log: (line) => logs.push(line), }) await srv.start() const url = `ws://127.0.0.1:${srv.boundPort}${PATH}` const clients = [] const mkNode = (hostId, secret, network, ports) => { const c = new RelayClient({ url, hostId, secret, ports, networkId: network, log: () => {} }) clients.push(c) c.start() return c } const mkDialer = (hostId, secret, network) => { const c = new RelayClient({ url, hostId, secret, ports: [], dialer: true, networkId: network, log: (line) => logs.push(`[${hostId}] ${line}`), }) clients.push(c) c.start() return c } mkNode('aw1', secrets.alpha, U_A, [portA]) mkNode('bw1', secrets.beta, U_B, [portB]) const ad = mkDialer('ad', secrets.alphaDialer, U_A) mkDialer('bd', secrets.betaDialer, U_B) mkDialer('manager', secrets.opsManager, OPS_NETWORK) t.after(async () => { for (const c of clients) c.stop() await echoA.close() await echoB.close() await srv.stop() }) assert.ok(await waitFor(() => srv.isOnline('aw1', U_A)), `u:alpha 的节点未注册:${logs.slice(-8).join(' | ')}`) assert.ok(await waitFor(() => srv.isOnline('bw1', U_B)), `u:beta 的节点未注册:${logs.slice(-8).join(' | ')}`) assert.ok(await waitFor(() => srv.isOnline('ad', U_A)), 'u:alpha 拨号方未注册') assert.ok(await waitFor(() => srv.isOnline('bd', U_B)), 'u:beta 拨号方未注册') // ── 正证:同网全通(隔离挡的是"跨网",不是"拨号"本身) ── const stream = await ad.openStream('aw1', portA) assert.equal( await dialRoundTrip(stream, 'ping-alpha', 'alpha:'.length), 'alpha:ping-alpha', '同网必须真的通(且字节走的是 u:alpha 自己那台,不是别网的同名机)', ) // ── 反证 ①:到不了 —— `u:alpha` 的拨号方够不到只在 `u:beta` 里的节点 ── let errForeign = '' await assert.rejects( () => ad.openStream('bw1', portB), (e) => { errForeign = e instanceof Error ? e.message : String(e) return true }, 'u:alpha 拨号方不得够到 u:beta 的节点', ) // ── 反证 ②:**看不到** —— "在别张网"与"根本不存在"的响应**逐字相同** ── // 拿一个哪儿都没有的 hostId 打一次:两者若不同形,就能拿一串猜测的 hostId 把别张网**枚举**出来。 let errGhost = '' await assert.rejects( () => ad.openStream('definitely-not-a-host', portB), (e) => { errGhost = e instanceof Error ? e.message : String(e) return true }, '不存在的 hostId 必须被拒', ) const norm = (m) => m.replaceAll('bw1', 'X').replaceAll('definitely-not-a-host', 'X') assert.equal( norm(errForeign), norm(errGhost), `"在别张网"与"不存在"必须逐字同形(否则可枚举对端清单):\n 在别网=${errForeign}\n 不存在=${errGhost}`, ) // 响应里不得出现任何网络名(`u:beta` 一旦出现 ⇒ 对端清单泄露) assert.equal(errForeign.includes(U_B), false, `响应不得含网络名,实测:${errForeign}`) assert.equal(errForeign.includes('dialer-not-in-network'), false, `对外不得有跨网专属拒码,实测:${errForeign}`) // ── 反证 ③:`ops` 对用户网**默认不可见**(反向也一样) ── // 日志里必须留得下区分(服务端可取证)—— 这是"看不到"与"查得到"的平衡点 assert.ok( logs.some((l) => l.includes('dialer-not-in-network') && l.includes(U_B)), `relay 日志必须能区分"在别张网",实测:${logs.filter((l) => l.includes('DIAL')).slice(-6).join(' | ')}`, ) }) /* ─────────── A7:范围声明(Step 3 只做 P0-3) ─────────── */ test('A7 范围:本步只做"逻辑名 + 授权",不碰 L3 / DNS / 对端清单(D1 / D4)', async () => { const net = await import('../lib/net/relay/network.js') assert.equal(typeof net.logicalName, 'function') assert.deepEqual(parseLogicalName(`${U_A}/w-1`), { network: U_A, hostId: 'w-1' }) // ⛔ 本阶段刻意**没有**的东西:地址段分配 / DNS 名 / 对端清单查询 assert.equal(net.assignAddress, undefined, 'P0-3 不做 L3 地址分配(D1:推迟到 L3 专项)') assert.equal(net.resolveDns, undefined, 'P0-3 不自建 DNS(D1)') assert.equal(net.listPeers, undefined, 'P0-3 不下发对端清单(D4)') })