Files
hy2-panel/tests/anytls-runtime.ts
T

179 lines
6.3 KiB
TypeScript

// 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<void>((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<void>((resolve) => p.once("exit", () => resolve()));
};
const transfer = () =>
new Promise<number>((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<void>((resolve) => target.close(() => resolve()));
(await ensureDb()).close();
rmSync(dir, { recursive: true, force: true });
}
}
main().catch((e) => {
console.error(e.message);
process.exitCode = 1;
});