feat: 接入 AnyTLS 节点托管和用户流量同步
This commit is contained in:
+68
-10
@@ -1,3 +1,7 @@
|
||||
import { buildSingBoxConfig } from "@/lib/anytls";
|
||||
import { eligibleAnyTLSUsers } from "@/lib/node-agent";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { randomBytes, randomUUID, createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { stringify } from "yaml";
|
||||
@@ -9,7 +13,7 @@ import {
|
||||
validateCertificate,
|
||||
type ManagedCertificate,
|
||||
} from "@/lib/certificates";
|
||||
import type { Hy2NodeRow } from "@/lib/store";
|
||||
import type { Hy2NodeRow, Hy2UserRow } from "@/lib/store";
|
||||
|
||||
export const portSchema = z
|
||||
.object({
|
||||
@@ -22,13 +26,27 @@ export const portSchema = z
|
||||
statsListenPort: z.number().int().min(1).max(65535),
|
||||
})
|
||||
.strict();
|
||||
export const anytlsPortSchema = z
|
||||
.object({
|
||||
domain: portSchema.shape.domain,
|
||||
port: portSchema.shape.hy2Port,
|
||||
listenPort: portSchema.shape.hy2Port,
|
||||
statsPort: portSchema.shape.hy2Port,
|
||||
controlPort: portSchema.shape.hy2Port,
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(v) => new Set([v.listenPort, v.statsPort, v.controlPort]).size === 3,
|
||||
);
|
||||
export const installationSchema = z.object({
|
||||
slug: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
|
||||
name: z.string().trim().min(1).max(100),
|
||||
protocol: z.enum(["hy2", "anytls"]).optional(),
|
||||
certificateId: z.coerce.number().int().positive(),
|
||||
});
|
||||
export type Installation = {
|
||||
id: string;
|
||||
protocol?: "hy2" | "anytls";
|
||||
token_hash: string;
|
||||
token_encrypted: string;
|
||||
certificate_id: number;
|
||||
@@ -91,8 +109,8 @@ export async function createInstallation(
|
||||
);
|
||||
db.prepare(
|
||||
`INSERT INTO node_installations
|
||||
(id,token_hash,token_encrypted,certificate_id,slug,name,server_host,sni,origin,expires_at,created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
(id,token_hash,token_encrypted,certificate_id,slug,name,server_host,sni,origin,expires_at,created_at,protocol)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
).run(
|
||||
id,
|
||||
hash(token),
|
||||
@@ -105,6 +123,7 @@ export async function createInstallation(
|
||||
origin.origin,
|
||||
new Date(Date.now() + 24 * 3600_000).toISOString(),
|
||||
now,
|
||||
input.protocol || "hy2",
|
||||
);
|
||||
}).immediate();
|
||||
return id;
|
||||
@@ -114,7 +133,7 @@ export async function listInstallations() {
|
||||
const db = await ensureDb();
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT id,slug,name,server_host,sni,expires_at,created_at,revoked_at,completed_at,node_id,
|
||||
`SELECT id,protocol,slug,name,server_host,sni,expires_at,created_at,revoked_at,completed_at,node_id,
|
||||
(SELECT name FROM managed_certificates WHERE id=certificate_id) AS certificate_name
|
||||
FROM node_installations ORDER BY created_at DESC`,
|
||||
)
|
||||
@@ -182,12 +201,25 @@ export async function provisionNode(
|
||||
}
|
||||
if (job.completed_at)
|
||||
throw new ProvisionError("该凭据已完成安装,不能再次获取私钥。");
|
||||
const parsed = portSchema.safeParse(rawPorts);
|
||||
const parsed =
|
||||
job.protocol === "anytls"
|
||||
? anytlsPortSchema.safeParse(rawPorts)
|
||||
: portSchema.safeParse(rawPorts);
|
||||
if (!parsed.success)
|
||||
throw new ProvisionError(
|
||||
"域名须为 .p.xn--876a.net 下的单级子域名,端口必须为 1–65535 的整数。",
|
||||
);
|
||||
const ports = parsed.data,
|
||||
const ports =
|
||||
"listenPort" in parsed.data
|
||||
? {
|
||||
domain: parsed.data.domain,
|
||||
hy2Port: parsed.data.port,
|
||||
hy2ListenPort: parsed.data.listenPort,
|
||||
statsPort: parsed.data.statsPort,
|
||||
statsListenPort: parsed.data.statsPort,
|
||||
controlPort: parsed.data.controlPort,
|
||||
}
|
||||
: { ...parsed.data, controlPort: 0 },
|
||||
portsJson = JSON.stringify(ports);
|
||||
if (job.ports_json && job.ports_json !== portsJson)
|
||||
throw new ProvisionError(
|
||||
@@ -209,14 +241,16 @@ export async function provisionNode(
|
||||
.prepare(
|
||||
`INSERT INTO hy2_nodes
|
||||
(slug,name,auth_token,traffic_stats_url,traffic_stats_secret,enabled,created_at,updated_at,
|
||||
server_host,server_port,client_name,sni,hy2_listen,traffic_stats_listen)
|
||||
VALUES (?,?,?,?,?,0,?,?,?,?,?,?,?,?)`,
|
||||
server_host,server_port,client_name,sni,hy2_listen,traffic_stats_listen,node_type)
|
||||
VALUES (?,?,?,?,?,0,?,?,?,?,?,?,?,?,?)`,
|
||||
)
|
||||
.run(
|
||||
job.slug,
|
||||
job.name,
|
||||
randomBytes(32).toString("hex"),
|
||||
`http://${host}:${ports.statsPort}`,
|
||||
job.protocol === "anytls"
|
||||
? `http://127.0.0.1:${ports.controlPort}`
|
||||
: `http://${host}:${ports.statsPort}`,
|
||||
randomBytes(32).toString("hex"),
|
||||
now,
|
||||
now,
|
||||
@@ -225,7 +259,10 @@ export async function provisionNode(
|
||||
job.name,
|
||||
host,
|
||||
`:${ports.hy2ListenPort}`,
|
||||
`:${ports.statsListenPort}`,
|
||||
job.protocol === "anytls"
|
||||
? `127.0.0.1:${ports.statsPort}`
|
||||
: `:${ports.statsListenPort}`,
|
||||
job.protocol || "hy2",
|
||||
).lastInsertRowid,
|
||||
);
|
||||
db.prepare(
|
||||
@@ -235,6 +272,27 @@ export async function provisionNode(
|
||||
const node = db
|
||||
.prepare("SELECT * FROM hy2_nodes WHERE id=?")
|
||||
.get(nodeId) as Hy2NodeRow;
|
||||
if (job.protocol === "anytls") {
|
||||
const users = eligibleAnyTLSUsers(
|
||||
db
|
||||
.prepare("SELECT * FROM hy2_users ORDER BY id")
|
||||
.all() as Hy2UserRow[],
|
||||
);
|
||||
return {
|
||||
config: JSON.stringify(buildSingBoxConfig(node, users), null, 2),
|
||||
certificate: cert.certificate_pem,
|
||||
privateKey: key,
|
||||
agent: { panel: job.origin, slug: job.slug, token: node.auth_token },
|
||||
agentSource: readFileSync(
|
||||
path.join(process.cwd(), "lib/templates/anytls-agent.py"),
|
||||
"utf8",
|
||||
),
|
||||
statsProto: readFileSync(
|
||||
path.join(process.cwd(), "lib/templates/stats.proto"),
|
||||
"utf8",
|
||||
),
|
||||
};
|
||||
}
|
||||
const config = stringify({
|
||||
listen: node.hy2_listen,
|
||||
tls: {
|
||||
|
||||
Reference in New Issue
Block a user