feat: 支持托管中转主机和节点接入线路
This commit is contained in:
@@ -29,6 +29,8 @@ function snapshot(db: Database.Database) {
|
||||
"managed_certificates",
|
||||
"node_installations",
|
||||
"node_agent_counters",
|
||||
"relay_hosts",
|
||||
"relay_routes",
|
||||
].includes(name),
|
||||
)
|
||||
.map(({ name }) => [name, db.prepare(`SELECT * FROM "${name}"`).all()]),
|
||||
@@ -49,7 +51,7 @@ test("fresh database exactly matches the inspected remote v4 column definitions"
|
||||
const fresh = new Database(":memory:");
|
||||
const remote = remoteDb();
|
||||
migrateDatabase(fresh);
|
||||
assert.equal(fresh.pragma("user_version", { simple: true }), 6);
|
||||
assert.equal(fresh.pragma("user_version", { simple: true }), 7);
|
||||
for (const { name } of remote
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.all() as { name: string }[]) {
|
||||
@@ -102,7 +104,7 @@ for (const existing of [
|
||||
if (!existing.includes(field)) delete row[field];
|
||||
}
|
||||
assert.deepEqual(after, before);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 6);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 7);
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
@@ -132,8 +134,34 @@ test("failed migrations roll back all statements and future schemas are not down
|
||||
db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(),
|
||||
[{ name: "hy2_users" }],
|
||||
);
|
||||
db.pragma("user_version = 7");
|
||||
db.pragma("user_version = 8");
|
||||
assert.throws(() => migrateDatabase(db), /版本/);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 8);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test("v6 upgrade preserves AnyTLS installations, certificates and counters", () => {
|
||||
const db = remoteDb();
|
||||
seed(db);
|
||||
migrateDatabase(db);
|
||||
db.exec(
|
||||
"DROP TABLE relay_routes; DROP TABLE relay_hosts; PRAGMA user_version=6;",
|
||||
);
|
||||
db.exec(`INSERT INTO managed_certificates VALUES(1,'wildcard','certificate','encrypted-key','2030-01-01','2026-01-01');
|
||||
INSERT INTO node_installations(id,token_hash,token_encrypted,certificate_id,slug,name,server_host,sni,origin,expires_at,created_at,node_id,protocol) VALUES('install','hash','encrypted',1,'anytls','AnyTLS','jp2.example','jp2.example','https://panel.example','2030-01-01','2026-01-01',17,'anytls');
|
||||
INSERT INTO node_agent_counters VALUES(17,'boot:1','original-auth',128,256);`);
|
||||
const tables = [
|
||||
"managed_certificates",
|
||||
"node_installations",
|
||||
"node_agent_counters",
|
||||
];
|
||||
const before = tables.map((t) => db.prepare(`SELECT * FROM ${t}`).all());
|
||||
migrateDatabase(db);
|
||||
migrateDatabase(db);
|
||||
assert.deepEqual(
|
||||
tables.map((t) => db.prepare(`SELECT * FROM ${t}`).all()),
|
||||
before,
|
||||
);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 7);
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
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 { parse } from "yaml";
|
||||
|
||||
test("relay routes inherit credentials and SNI, isolate polling, reject conflicts and withdraw stale/disabled routes", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "hy2-relay-"));
|
||||
process.env.DATABASE_PATH = path.join(dir, "db.sqlite");
|
||||
const { ensureDb } = await import("../../lib/db");
|
||||
const {
|
||||
createNode,
|
||||
getNodeBySlug,
|
||||
listSubscriptionNodes,
|
||||
listEnabledNodesForPolling,
|
||||
listUsers,
|
||||
getSubscriptionSettings,
|
||||
} = await import("../../lib/store");
|
||||
const {
|
||||
saveRelayHost,
|
||||
saveRelayRoute,
|
||||
relayInventory,
|
||||
syncRelay,
|
||||
relayDesired,
|
||||
deleteRelay,
|
||||
} = await import("../../lib/relays");
|
||||
const { decryptText } = await import("../../lib/crypto");
|
||||
const { buildSubscriptionConfig } = await import("../../lib/subscription");
|
||||
const db = await ensureDb();
|
||||
try {
|
||||
for (const protocol of ["hy2", "anytls"] as const) {
|
||||
await createNode({
|
||||
nodeType: protocol,
|
||||
slug: protocol,
|
||||
name: protocol,
|
||||
authToken: null,
|
||||
hy2Listen: ":51046",
|
||||
serverHost: "jp2.p.xn--876a.net",
|
||||
serverPort: protocol === "hy2" ? 51044 : 51046,
|
||||
clientName: protocol,
|
||||
bandwidthUpMbps: 30,
|
||||
bandwidthDownMbps: 30,
|
||||
skipCertVerify: false,
|
||||
sni: null,
|
||||
trafficStatsUrl: "http://127.0.0.1:19090",
|
||||
trafficStatsListen: ":19090",
|
||||
trafficStatsSecret: "test",
|
||||
enabled: true,
|
||||
pollIntervalSeconds: 15,
|
||||
});
|
||||
}
|
||||
const any = (await getNodeBySlug("anytls"))!,
|
||||
hy2 = (await getNodeBySlug("hy2"))!;
|
||||
const hostId = await saveRelayHost({
|
||||
name: "HK2",
|
||||
server_host: "hk2.p.xn--876a.net",
|
||||
enabled: true,
|
||||
});
|
||||
const host = (await relayInventory()).hosts[0],
|
||||
token = decryptText(host.token_encrypted)!;
|
||||
const base = {
|
||||
host_id: hostId,
|
||||
node_id: any.id,
|
||||
name: "JP2 via HK2 TCP",
|
||||
public_port: 52046,
|
||||
listen_port: 51046,
|
||||
enabled: true,
|
||||
};
|
||||
const tcpId = await saveRelayRoute(base);
|
||||
await saveRelayRoute({
|
||||
...base,
|
||||
node_id: hy2.id,
|
||||
name: "JP2 via HK2 UDP",
|
||||
public_port: 52046,
|
||||
listen_port: 51046,
|
||||
});
|
||||
assert.equal(
|
||||
(await listSubscriptionNodes()).length,
|
||||
2,
|
||||
"not advertised before application",
|
||||
);
|
||||
await assert.rejects(
|
||||
() => saveRelayRoute({ ...base, name: "conflict" }),
|
||||
/端口/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
saveRelayRoute({
|
||||
...base,
|
||||
public_port: 52047,
|
||||
listen_port: 51047,
|
||||
name: "anytls",
|
||||
}),
|
||||
/重复/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
syncRelay(hostId, "0".repeat(64), {
|
||||
appliedHash: "",
|
||||
active: false,
|
||||
error: "",
|
||||
}),
|
||||
/凭据/,
|
||||
);
|
||||
const desired = await syncRelay(hostId, token, {
|
||||
appliedHash: "",
|
||||
active: false,
|
||||
error: "",
|
||||
});
|
||||
assert.equal(desired.config.endpoints.length, 2);
|
||||
assert.equal(
|
||||
desired.config.endpoints.find((e) => e.remote.endsWith(":51044"))?.network
|
||||
.no_tcp,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
desired.config.endpoints.find((e) => e.remote.endsWith(":51046"))?.network
|
||||
.use_udp,
|
||||
false,
|
||||
);
|
||||
assert.ok(
|
||||
!JSON.stringify(desired).includes(any.auth_token),
|
||||
"relay receives no backend credentials",
|
||||
);
|
||||
await syncRelay(hostId, token, {
|
||||
appliedHash: desired.hash,
|
||||
active: true,
|
||||
error: "",
|
||||
});
|
||||
const nodes = await listSubscriptionNodes();
|
||||
assert.equal(nodes.length, 4);
|
||||
assert.equal(
|
||||
(await listEnabledNodesForPolling()).length,
|
||||
1,
|
||||
"only original HY2 is polled",
|
||||
);
|
||||
assert.equal(
|
||||
(db.prepare("SELECT count(*) n FROM hy2_nodes").get() as { n: number }).n,
|
||||
2,
|
||||
);
|
||||
const result = parse(
|
||||
buildSubscriptionConfig({
|
||||
user: (await listUsers())[0],
|
||||
nodes,
|
||||
settings: await getSubscriptionSettings(),
|
||||
}),
|
||||
);
|
||||
const direct = result.proxies.find(
|
||||
(p: { name: string }) => p.name === "anytls",
|
||||
),
|
||||
relay = result.proxies.find(
|
||||
(p: { name: string }) => p.name === base.name,
|
||||
);
|
||||
assert.equal(relay.password, direct.password);
|
||||
assert.equal(relay.sni, "jp2.p.xn--876a.net");
|
||||
assert.equal(relay.server, "hk2.p.xn--876a.net");
|
||||
assert.equal(relay.port, 52046);
|
||||
assert.equal(relay.udp, true);
|
||||
db.prepare("UPDATE relay_hosts SET last_seen_at=? WHERE id=?").run(
|
||||
"2000-01-01T00:00:00.000Z",
|
||||
hostId,
|
||||
);
|
||||
assert.equal((await listSubscriptionNodes()).length, 2);
|
||||
await syncRelay(hostId, token, {
|
||||
appliedHash: desired.hash,
|
||||
active: true,
|
||||
error: "port_conflict",
|
||||
});
|
||||
assert.equal((await listSubscriptionNodes()).length, 2);
|
||||
await syncRelay(hostId, token, {
|
||||
appliedHash: desired.hash,
|
||||
active: true,
|
||||
error: "",
|
||||
});
|
||||
await saveRelayRoute({ ...base, id: tcpId, enabled: false });
|
||||
assert.equal(
|
||||
(await listSubscriptionNodes()).length,
|
||||
2,
|
||||
"hide old generation until applied",
|
||||
);
|
||||
const next = relayDesired(db, (await relayInventory()).hosts[0]);
|
||||
assert.equal(next.config.endpoints.length, 1);
|
||||
await syncRelay(hostId, token, {
|
||||
appliedHash: next.hash,
|
||||
active: true,
|
||||
error: "",
|
||||
});
|
||||
assert.equal((await listSubscriptionNodes()).length, 3);
|
||||
await saveRelayHost({
|
||||
id: hostId,
|
||||
name: "HK2",
|
||||
server_host: "hk2.p.xn--876a.net",
|
||||
enabled: false,
|
||||
});
|
||||
assert.equal(
|
||||
(
|
||||
await syncRelay(hostId, token, {
|
||||
appliedHash: next.hash,
|
||||
active: true,
|
||||
error: "",
|
||||
})
|
||||
).config.endpoints.length,
|
||||
0,
|
||||
);
|
||||
assert.equal((await listSubscriptionNodes()).length, 2);
|
||||
await deleteRelay("host", hostId);
|
||||
assert.equal((await relayInventory()).routes.length, 0);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
syncRelay(hostId, token, { appliedHash: "", active: false, error: "" }),
|
||||
/凭据/,
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user