feat: 接入 AnyTLS 节点托管和用户流量同步
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// 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;
|
||||
});
|
||||
Reference in New Issue
Block a user