feat: 接入 AnyTLS 节点托管和用户流量同步

This commit is contained in:
2026-09-29 13:35:44 +08:00
parent 586c2399d1
commit ac5fed695c
24 changed files with 1413 additions and 185 deletions
+162
View File
@@ -0,0 +1,162 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { mkdtempSync, rmSync } from "node:fs";
import path from "node:path";
import { tmpdir } from "node:os";
import { certificateFixture } from "../certificate-fixture";
import { parse } from "yaml";
test("AnyTLS provisioning, per-user credentials, cumulative accounting and revocation", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "hy2-anytls-"));
process.env.DATABASE_PATH = path.join(dir, "db.sqlite");
process.env.APP_URL = "https://panel.example.com";
const { ensureDb } = await import("../../lib/db");
const { encryptText, decryptText } = await import("../../lib/crypto");
const { saveCertificate } = await import("../../lib/certificates");
const { createInstallation, installationForDownload, provisionNode } =
await import("../../lib/provisioning");
const { syncAnyTLSNode } = await import("../../lib/node-agent");
const { anytlsPassword } = await import("../../lib/anytls");
const { buildInstallScript } = await import("../../lib/install-script");
const {
getNodeBySlug,
listUsers,
getSubscriptionSettings,
listEnabledNodesForPolling,
} = await import("../../lib/store");
const { buildSubscriptionConfig } = await import("../../lib/subscription");
const db = await ensureDb();
try {
const fixture = certificateFixture();
const certificateId = Number(
await saveCertificate(
"Wildcard",
fixture.certificate,
fixture.privateKey,
),
);
const id = await createInstallation({
slug: "anytls-test",
name: "AnyTLS",
certificateId,
protocol: "anytls",
});
const job = await installationForDownload(id);
assert.equal(job.protocol, "anytls");
assert.match(buildInstallScript(job), /--port/);
const installToken = decryptText(job.token_encrypted)!;
const ports = {
domain: "jp2.p.xn--876a.net",
port: 51046,
listenPort: 51046,
statsPort: 19090,
controlPort: 19091,
};
await assert.rejects(
() =>
provisionNode(installToken, "prepare", {
...ports,
controlPort: 19090,
}),
/端口/,
);
const bundle = await provisionNode(installToken, "prepare", ports);
assert.ok(bundle.agent && bundle.config && bundle.agentSource);
assert.equal(bundle.agent.slug, "anytls-test");
assert.match(bundle.agentSource, /reset=False/);
const initial = JSON.parse(bundle.config);
assert.equal(initial.inbounds[0].type, "anytls");
assert.equal(initial.inbounds[0].listen_port, 51046);
assert.equal(initial.experimental.v2ray_api.listen, "127.0.0.1:19090");
const user = (await listUsers())[0];
const password = anytlsPassword(user);
assert.equal(initial.inbounds[0].users[0].password, password);
assert.notEqual(password, decryptText(user.auth_secret_encrypted));
await provisionNode(installToken, "complete");
const node = (await getNodeBySlug(job.slug))!;
assert.equal(node.node_type, "anytls");
assert.equal((await listEnabledNodesForPolling()).length, 0);
const subscription = parse(
buildSubscriptionConfig({
user,
nodes: [node],
settings: await getSubscriptionSettings(),
}),
);
assert.equal(subscription.proxies[0].type, "anytls");
assert.equal(subscription.proxies[0].password, password);
assert.equal(subscription.proxies[0]["skip-cert-verify"], false);
const report = {
epoch: "boot:123:456",
traffic: { [user.auth_id]: { tx: 1000, rx: 500 } },
streamCount: 2,
};
await assert.rejects(
() => syncAnyTLSNode(job.slug, "0".repeat(64), report),
/凭据/,
);
await assert.rejects(
() =>
syncAnyTLSNode(job.slug, bundle.agent.token, {
...report,
traffic: { [user.auth_id]: { tx: -1, rx: 2 } },
}),
/无效/,
);
await syncAnyTLSNode(job.slug, bundle.agent.token, report);
await syncAnyTLSNode(job.slug, bundle.agent.token, report);
let refreshed = (await listUsers())[0];
assert.equal(refreshed.used_tx_bytes, 1000);
assert.equal(refreshed.used_rx_bytes, 500);
await syncAnyTLSNode(job.slug, bundle.agent.token, {
...report,
traffic: { [user.auth_id]: { tx: 900, rx: 400 } },
});
refreshed = (await listUsers())[0];
assert.equal(refreshed.used_tx_bytes, 1000);
await syncAnyTLSNode(job.slug, bundle.agent.token, {
...report,
epoch: "boot:124:500",
traffic: { [user.auth_id]: { tx: 100, rx: 50 } },
});
refreshed = (await listUsers())[0];
assert.equal(refreshed.used_tx_bytes, 1100);
// Make this account an ordinary user to exercise quota/expiry/disable rules.
db.prepare(
"UPDATE hy2_users SET is_admin=0,traffic_limit_bytes=1000 WHERE id=?",
).run(user.id);
const capped = await syncAnyTLSNode(job.slug, bundle.agent.token, report);
assert.equal(capped.config.inbounds[0].users.length, 0);
db.prepare(
"UPDATE hy2_users SET traffic_limit_bytes=NULL,expires_at='2000-01-01' WHERE id=?",
).run(user.id);
assert.equal(
(await syncAnyTLSNode(job.slug, bundle.agent.token, report)).config
.inbounds[0].users.length,
0,
);
db.prepare("UPDATE hy2_users SET expires_at=NULL,enabled=0 WHERE id=?").run(
user.id,
);
assert.equal(
(await syncAnyTLSNode(job.slug, bundle.agent.token, report)).config
.inbounds[0].users.length,
0,
);
db.prepare(
"UPDATE hy2_users SET enabled=1,auth_secret_encrypted=? WHERE id=?",
).run(encryptText("new-password"), user.id);
const updated = await syncAnyTLSNode(job.slug, bundle.agent.token, report);
assert.notEqual(updated.config.inbounds[0].users[0].password, password);
db.prepare("UPDATE hy2_nodes SET enabled=0 WHERE id=?").run(node.id);
assert.equal(
(await syncAnyTLSNode(job.slug, bundle.agent.token, report)).config
.inbounds[0].users.length,
0,
);
assert.deepEqual(db.pragma("foreign_key_check"), []);
} finally {
db.close();
rmSync(dir, { recursive: true, force: true });
}
});