Files
nodeagent/scripts/verify-b2.mjs
T
jkooe b9236dfaf1 chore(privacy): 公开仓库隐私脱敏 —— 清除可定位到个人与设备的字面量
按 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。
2026-10-13 22:35:40 +08:00

161 lines
5.5 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.
#!/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);
}