按 security-reports/ 的隐私审查台账逐项收口。纯脱敏与表述泛化, 不含任何业务逻辑变更;实测结论与排查步骤一字未动。 P1(可定位到具体设备/个人) - scripts/verify-b2.mjs:NA_URL 去掉硬编码缺省值(原指向一台真实机器),改为必填。 这是全仓库唯一「拿着就能连到某台真机」的常量。 - 测试账户名 pdverify → na-test(docs/VM-VERIFICATION.md 7 处、docs/console-plan.md 1 处) - 虚拟机与宿主的真实网段 → RFC 5737 文档保留段 192.0.2.x(含 CLOSE_WAIT 记录) - 跨项目名与私人称呼 → 中性表述(docs/PRD.md、docs/DEVELOPMENT.md、docs/console-plan.md 等) P2(低风险设备指纹) - 虚拟化产品及其 CLI / 访客工具名 → 中性表述(12 个文件 + 1 个工作流注释) - 测试目录 C:\nodeagent-test → C:\na-test - NAS 设备型号与系统名 → NAS 设备 / NAS 系统(docs/DEPLOY-LINUX.md、scripts/install-linux.sh、README.md) - 真机 CPU 型号、真机物理分辨率、代理软件品牌与监听端口、杀软品牌 → 泛化 - 单元测试示例段去掉真机 IP 尾八位:acl-v11 / net-scripts 的 fixture 与断言同步更新 - .gitignore 补 config.json / agent.json(含真实密钥与节点地址,绝不入库) - 顺带移除混入技术文档的「不构成投资建议」字样(与文档主题无关) 验证:pnpm test:unit 340/340 通过;独立扫描器对 235 个跟踪文件做 17 类模式 全量扫描,命中 0。
161 lines
5.5 KiB
JavaScript
161 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* B2 验证脚本:v23 连接层三防护(真机跑,不是 mock)。
|
||
*
|
||
* 为什么单独写:CLI 的命令都是**短连接**(连完即断),测不出「空闲断开」;
|
||
* 而 `events` 订阅会让服务端认为"该连接有订阅、不能踢"(这是 v23 的保守设计)。
|
||
* 所以需要一个**握手完成后什么都不做**的长连接。
|
||
*
|
||
* 用法:
|
||
* node /tmp/b2-idle.mjs idle # ③ 空闲断开(观察是否 ~idle_timeout 被断)
|
||
* node /tmp/b2-idle.mjs maxconn # ② 连接上限(并发 N+1 个,第 N+1 应被拒)
|
||
*/
|
||
|
||
// 用相对路径:从仓库根跑时解析不到 workspace 包(根 package.json 未声明它)
|
||
import { NodeAgentClient } from '../packages/client/dist/index.js';
|
||
|
||
// ⚠️ 必须 wss:被控端 tls=true(写成 ws 会 socket hang up)
|
||
const URL = process.env.NA_URL;
|
||
const KEY = process.env.NA_KEY;
|
||
if (!URL) {
|
||
console.error('需要 NA_URL 环境变量,例如 wss://<被控端地址>:8765');
|
||
process.exit(1);
|
||
}
|
||
if (!KEY) {
|
||
console.error('需要 NA_KEY 环境变量');
|
||
process.exit(1);
|
||
}
|
||
|
||
const mode = process.argv[2] ?? 'idle';
|
||
const ts = () => new Date().toISOString().slice(11, 19);
|
||
|
||
function mkClient(id) {
|
||
return new NodeAgentClient({
|
||
url: URL,
|
||
key: KEY,
|
||
clientId: id,
|
||
insecure: true,
|
||
handshakeTimeoutMs: 10_000,
|
||
onStateChange: (s) => console.log(`[${ts()}] ${id} state=${s}`),
|
||
onLog: (m) => console.log(`[${ts()}] ${id} log: ${m}`),
|
||
});
|
||
}
|
||
|
||
// ---------------- ③ 空闲断开 ----------------
|
||
async function idleTest() {
|
||
const started = Date.now();
|
||
const c = mkClient('idle-probe');
|
||
try {
|
||
await c.connect();
|
||
} catch (err) {
|
||
console.log(`[${ts()}] ⚠️ 握手失败:${err?.message ?? err}`);
|
||
console.log(`[${ts()}] 提示:若报 socket hang up,通常说明已达 max_connections(先等空闲超时清掉残留)`);
|
||
process.exit(3);
|
||
}
|
||
console.log(`[${ts()}] ${'idle-probe'} 已握手完成(t=0s),之后**不做任何请求、不订阅**`);
|
||
console.log(`[${ts()}] 期待:约 idle_timeout_ms 后被服务端断开(state=closed)`);
|
||
|
||
await new Promise((resolve) => {
|
||
const timer = setInterval(() => {
|
||
const sec = ((Date.now() - started) / 1000).toFixed(0);
|
||
if (sec % 10 === '0') console.log(`[${ts()}] t=${sec}s 仍存活`);
|
||
if (Number(sec) >= 75) {
|
||
clearInterval(timer);
|
||
console.log(`[${ts()}] t=${sec}s —— 75 秒到,**未被断开**(空闲断开未生效或阈值更长)`);
|
||
resolve();
|
||
}
|
||
}, 1000);
|
||
// 订阅状态变化:closed 即被服务端断开
|
||
const orig = c.opts?.onStateChange;
|
||
void orig;
|
||
});
|
||
|
||
try {
|
||
c.close();
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
console.log(`[${ts()}] 结束`);
|
||
process.exit(0);
|
||
}
|
||
|
||
// ---------------- ② 连接上限 ----------------
|
||
async function maxConnTest() {
|
||
const n = Number(process.env.NA_MAX ?? 2);
|
||
const clients = [];
|
||
console.log(`[${ts()}] 尝试建立 ${n + 1} 个并发连接(配置 max_connections=${n})`);
|
||
|
||
for (let i = 1; i <= n + 1; i += 1) {
|
||
const id = `conn-${i}`;
|
||
const c = mkClient(id);
|
||
try {
|
||
await c.connect();
|
||
clients.push(c);
|
||
console.log(`[${ts()}] ✓ 第 ${i} 个连接成功`);
|
||
} catch (err) {
|
||
console.log(`[${ts()}] ✗ 第 ${i} 个连接被拒:${err?.message ?? err}`);
|
||
// 第 n+1 个被拒即为通过
|
||
for (const x of clients) {
|
||
try {
|
||
x.close();
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
const pass = i === n + 1;
|
||
console.log(`[${ts()}] 结论:${pass ? '✓ 连接上限生效' : '⚠️ 被拒的不是第 N+1 个'}`);
|
||
process.exit(pass ? 0 : 2);
|
||
}
|
||
// 稍等,避免瞬时并发被当成同一个
|
||
await new Promise((r) => setTimeout(r, 400));
|
||
}
|
||
|
||
console.log(`[${ts()}] ⚠️ ${n + 1} 个连接全部成功 —— 连接上限未生效`);
|
||
for (const x of clients) {
|
||
try {
|
||
x.close();
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
process.exit(2);
|
||
}
|
||
|
||
|
||
// ---------------- ① 握手封禁 ----------------
|
||
async function banTest() {
|
||
const n = Number(process.env.NA_ATTEMPTS ?? 3);
|
||
console.log(`[${ts()}] 用**错误密钥**连 ${n} 次(配置 max_attempts=${n})`);
|
||
for (let i = 1; i <= n; i += 1) {
|
||
const c = new NodeAgentClient({ url: URL, key: 'WRONG-KEY-' + i, clientId: 'ban-' + i, insecure: true, handshakeTimeoutMs: 8000 });
|
||
try {
|
||
await c.connect();
|
||
console.log(`[${ts()}] ⚠️ 第 ${i} 次错误密钥竟然成功(不应发生)`);
|
||
c.close();
|
||
} catch (err) {
|
||
console.log(`[${ts()}] ✓ 第 ${i} 次被拒:${String(err?.message ?? err).slice(0, 60)}`);
|
||
}
|
||
await new Promise((r) => setTimeout(r, 500));
|
||
}
|
||
console.log(`[${ts()}] 现在用**正确密钥**连(若封禁生效,应当也被拒)`);
|
||
const good = new NodeAgentClient({ url: URL, key: KEY, clientId: 'ban-good', insecure: true, handshakeTimeoutMs: 8000 });
|
||
try {
|
||
await good.connect();
|
||
console.log(`[${ts()}] ✗ 正确密钥连接成功 —— **封禁未生效**`);
|
||
good.close();
|
||
process.exit(2);
|
||
} catch (err) {
|
||
console.log(`[${ts()}] ✓ 正确密钥也被拒:${String(err?.message ?? err).slice(0, 70)}`);
|
||
console.log(`[${ts()}] 结论:✓ 握手失败封禁生效(连正确密钥都被挡)`);
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
if (mode === 'idle') await idleTest();
|
||
else if (mode === 'maxconn') await maxConnTest();
|
||
else if (mode === 'ban') await banTest();
|
||
else {
|
||
console.error('用法: node b2-idle.mjs <idle|maxconn>');
|
||
process.exit(1);
|
||
}
|