// Optional integration check: SING_BOX_TEST_BINARY=/path/to/build npx tsx tests/anytls-runtime.ts import assert from "node:assert/strict"; import { spawn, execFileSync, type ChildProcess } from "node:child_process"; import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; import path from "node:path"; import { tmpdir } from "node:os"; import { createServer } from "node:http"; import { certificateFixture } from "./certificate-fixture"; async function main() { const binary = process.env.SING_BOX_TEST_BINARY; const python = process.env.AGENT_TEST_PYTHON; if (!binary || !python) throw new Error("Set SING_BOX_TEST_BINARY and AGENT_TEST_PYTHON"); const dir = mkdtempSync(path.join(tmpdir(), "hy2-anytls-runtime-")); process.env.DATABASE_PATH = path.join(dir, "db.sqlite"); process.env.APP_URL = "https://panel.example.com"; const { saveCertificate } = await import("../lib/certificates"); const { createInstallation, installationForDownload, provisionNode } = await import("../lib/provisioning"); const { decryptText } = await import("../lib/crypto"); const { syncAnyTLSNode } = await import("../lib/node-agent"); const { ensureDb } = await import("../lib/db"); const fixture = certificateFixture(); const certId = Number( await saveCertificate("Runtime", fixture.certificate, fixture.privateKey), ); const id = await createInstallation({ slug: "runtime", name: "Runtime", certificateId: certId, protocol: "anytls", }); const token = decryptText( (await installationForDownload(id)).token_encrypted, )!; const bundle = await provisionNode(token, "prepare", { domain: "runtime.p.xn--876a.net", port: 39194, listenPort: 39194, statsPort: 39196, controlPort: 39197, }); assert.ok(bundle.agent && bundle.config && bundle.statsProto); writeFileSync(path.join(dir, "server.crt"), fixture.certificate); writeFileSync(path.join(dir, "server.key"), fixture.privateKey, { mode: 0o600, }); const config = JSON.parse( bundle.config.replaceAll("/etc/hy2-panel-anytls", dir), ); const authId = config.inbounds[0].users[0].name; writeFileSync(path.join(dir, "server.json"), JSON.stringify(config), { mode: 0o600, }); writeFileSync( path.join(dir, "client.json"), JSON.stringify({ log: { level: "error" }, inbounds: [{ type: "mixed", listen: "127.0.0.1", listen_port: 39193 }], outbounds: [ { type: "anytls", server: "127.0.0.1", server_port: 39194, password: config.inbounds[0].users[0].password, tls: { enabled: true, server_name: "runtime.p.xn--876a.net", certificate_path: path.join(dir, "server.crt"), }, }, ], }), { mode: 0o600 }, ); const target = createServer((_req, res) => res.end("A".repeat(65536))); await new Promise((resolve) => target.listen(39195, "127.0.0.1", resolve), ); const start = (file: string) => spawn(binary, ["run", "-c", path.join(dir, file)], { stdio: "ignore" }); let server: ChildProcess | undefined, client: ChildProcess | undefined; const stop = async (p?: ChildProcess) => { if (!p || p.exitCode !== null) return; p.kill(); await new Promise((resolve) => p.once("exit", () => resolve())); }; const transfer = () => new Promise((resolve) => { const p = spawn( "curl", [ "--noproxy", "", "-fsS", "--max-time", "4", "--socks5-hostname", "127.0.0.1:39193", "http://127.0.0.1:39195", "-o", "/dev/null", ], { stdio: "ignore" }, ); p.once("exit", (code) => resolve(code ?? 1)); }); try { execFileSync(binary, ["check", "-c", path.join(dir, "server.json")], { stdio: "ignore", }); server = start("server.json"); client = start("client.json"); await new Promise((r) => setTimeout(r, 800)); assert.equal(await transfer(), 0, "real AnyTLS transfer"); writeFileSync(path.join(dir, "stats.proto"), bundle.statsProto); execFileSync(python, [ "-m", "grpc_tools.protoc", "-I", dir, `--python_out=${dir}`, path.join(dir, "stats.proto"), ]); writeFileSync( path.join(dir, "query.py"), `import grpc,json,stats_pb2\nwith grpc.insecure_channel('127.0.0.1:39196') as c:\n q=c.unary_unary('/v2ray.core.app.stats.command.StatsService/QueryStats',request_serializer=stats_pb2.QueryStatsRequest.SerializeToString,response_deserializer=stats_pb2.QueryStatsResponse.FromString)\n print(json.dumps({s.name:s.value for s in q(stats_pb2.QueryStatsRequest(pattern='user>>>'),timeout=5).stat}))\n`, ); const counters = JSON.parse( execFileSync(python, [path.join(dir, "query.py")], { encoding: "utf8" }), ); const tx = counters[`user>>>${authId}>>>traffic>>>downlink`], rx = counters[`user>>>${authId}>>>traffic>>>uplink`]; assert.ok(tx >= 65536 && rx > 0); await provisionNode(token, "complete"); await syncAnyTLSNode("runtime", bundle.agent.token, { epoch: "native:1", traffic: { [authId]: { tx, rx } }, streamCount: 0, }); const db = await ensureDb(); db.prepare("UPDATE hy2_users SET enabled=0 WHERE auth_id=?").run(authId); const disabled = await syncAnyTLSNode("runtime", bundle.agent.token, { epoch: "native:1", traffic: { [authId]: { tx, rx } }, streamCount: 0, }); await stop(server); writeFileSync( path.join(dir, "server.json"), JSON.stringify(disabled.config).replaceAll("/etc/hy2-panel-anytls", dir), { mode: 0o600 }, ); server = start("server.json"); await new Promise((r) => setTimeout(r, 500)); assert.notEqual(await transfer(), 0, "disabled credentials must fail"); console.log( JSON.stringify({ anytlsTransfer: true, tlsVerified: true, perUserTx: tx, perUserRx: rx, disabledUserRejected: true, }), ); } finally { await stop(client); await stop(server); await new Promise((resolve) => target.close(() => resolve())); (await ensureDb()).close(); rmSync(dir, { recursive: true, force: true }); } } main().catch((e) => { console.error(e.message); process.exitCode = 1; });