219 lines
6.2 KiB
TypeScript
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 });
|
|
}
|
|
});
|