Files
dsh_shenxian/test/overlay-auth.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

407 lines
18 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.
/**
* 覆盖网络 · **② P0-3 名字解析与授权** 单测 —— 逻辑名是不是"唯一入口"、跨网是不是**真拒绝**。
*
* ## 这个文件要回答的两个问题(= 交接单 §4 Step 3 的两条 + §5 判据 3)
* 1. **`place` / `resolve` 的唯一入口收不收逻辑名** `<network_id>/<hostId>`?
* —— 只收裸 `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 都回 `<tag>:<原字节>`。
* ⚠️ 与 `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,
/**
* ⚠️ **序③ 起:密钥表是成员资格的唯一判据** —— 键用**逻辑名** `<网>/<hostId>`(`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)')
})