179 lines
6.3 KiB
TypeScript
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;
|
|
});
|