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 }); } });