Files

219 lines
6.2 KiB
TypeScript

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