Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/live-vless-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Authorized live VLESS verification

This test is deliberately separate from secretless PR CI. `pnpm verify` never invokes it, and the
release workflow never receives its credentials. Never print or record the subscription URL, node
address, UUID, authentication material, or observed IP address.

## Reproduce

Install the repository-pinned official Mihomo asset with `egresskit runtime install`, then provide
the resulting executable path, an authorized HTTPS subscription, and an HTTPS JSON target whose
response has an `ip` field:

```sh
export EGRESSKIT_MIHOMO_BINARY=/private/path/to/mihomo
export EGRESSKIT_LIVE_SUBSCRIPTION_URL='set-in-your-secret-store'
export EGRESSKIT_LIVE_TARGET_URL='https://your-authorized-ip-echo.example/json'
pnpm test:live:vless
```

The runner uses an isolated temporary state directory, generated one-run admin and proxy tokens,
the real `egressd` and `egresskit` entry points, and the EgressKit proxy as the single egress
endpoint. It imports the remote subscription, lets official Mihomo check and apply it, makes a
direct target request, then makes the same request through an authenticated CONNECT tunnel. It
reports only four independent outcomes: local implementation, Mihomo acceptance, target
reachability, and real exit verification. Real exit verification passes only when the target
returns a valid proxy-observed IP distinct from the direct IP. Temporary state is removed.

## Evidence: 2026-09-08

- EgressKit commit under test: `4e97483` (the merged Issue #22 baseline).
- Platform: Darwin 25.6.0 arm64; Node.js v26.8.1.
- Official runtime: Mihomo Meta v1.19.30, Darwin arm64; repository-pinned SHA-256 verified.
- Configuration category: remote Mihomo YAML containing 170 VLESS nodes using TCP transport.
- Local implementation: passed; daemon started and the subscription operation completed.
- Mihomo acceptance: passed; the official runtime checked and applied the generated configuration.
- Target reachability: passed through the authenticated EgressKit CONNECT endpoint.
- Real exit verification: passed; the target observed a valid IP distinct from the direct exit.

No subscription URL, node secret, node endpoint, UUID, token, direct IP, or observed exit IP is
stored in this evidence.
37 changes: 37 additions & 0 deletions docs/live-vless-verification.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";

const root = new URL("../", import.meta.url);

test("authorized VLESS verification is documented and isolated from secretless CI", async () => {
const [documentation, packageJson, workflow] = await Promise.all([
readFile(new URL("live-vless-verification.md", import.meta.url), "utf8"),
readFile(new URL("package.json", root), "utf8").then(JSON.parse),
readFile(new URL(".github/workflows/release.yml", root), "utf8"),
]);

assert.match(documentation, /EGRESSKIT_LIVE_SUBSCRIPTION_URL/);
assert.match(documentation, /local implementation/i);
assert.match(documentation, /Mihomo acceptance/i);
assert.match(documentation, /target reachability/i);
assert.match(documentation, /real exit verification/i);
assert.match(documentation, /never (?:print|record).*subscription/i);
assert.equal(packageJson.scripts["test:live:vless"], "node scripts/live-vless.mjs");
assert.doesNotMatch(packageJson.scripts.verify, /test:live:vless/);
assert.doesNotMatch(workflow, /EGRESSKIT_LIVE_SUBSCRIPTION_URL|test:live:vless/);
});

test("the checked-in evidence is reproducible metadata without node secrets", async () => {
const documentation = await readFile(
new URL("live-vless-verification.md", import.meta.url),
"utf8",
);

assert.match(documentation, /2026-09-08/);
assert.match(documentation, /Mihomo Meta v1\.19\.30/);
assert.match(documentation, /Darwin 25\.6\.0 arm64/);
assert.match(documentation, /170 VLESS nodes/);
assert.doesNotMatch(documentation, /\/sub\/[a-z0-9]+/i);
assert.doesNotMatch(documentation, /uuid\s*[:=]/i);
});
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
"check": "pnpm typecheck",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"lint:root": "biome check package.json pnpm-workspace.yaml turbo.json biome.json tsconfig.base.json .github/workflows/release.test.mjs docker/Dockerfile.test.mjs docs/deployment.test.mjs release",
"lint:root": "biome check package.json pnpm-workspace.yaml turbo.json biome.json tsconfig.base.json .github/workflows/release.test.mjs docker/Dockerfile.test.mjs docs/*.test.mjs release scripts",
"prepare": "husky",
"test": "pnpm --filter @egresskit/egressd test && node --test .github/workflows/*.test.mjs apps/egressd/*.test.mjs docker/*.test.mjs docs/*.test.mjs release/*.test.mjs",
"test": "pnpm --filter @egresskit/egressd test && node --test .github/workflows/*.test.mjs apps/egressd/*.test.mjs docker/*.test.mjs docs/*.test.mjs release/*.test.mjs scripts/*.test.mjs",
"test:live:vless": "node scripts/live-vless.mjs",
"typecheck": "turbo run typecheck",
"verify": "pnpm lint && pnpm typecheck && pnpm test"
},
Expand Down
189 changes: 189 additions & 0 deletions scripts/live-vless.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { spawn, spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import { isIP } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";

export function cleanChildEnvironment(environment) {
return Object.fromEntries(
Object.entries(environment).filter(([name]) => !name.startsWith("EGRESSKIT_")),
);
}

export function validateTargetUrl(value) {
const target = new URL(value);
if (target.protocol !== "https:") throw new Error("live target must use HTTPS");
return target;
}

export function isVerifiedExit(direct, observed) {
return isIP(direct) !== 0 && isIP(observed) !== 0 && direct !== observed;
}

function required(name) {
const value = process.env[name];
if (!value) throw new Error(`${name} is required for the authorized live test`);
return value;
}

function run(command, args, options = {}) {
const result = spawnSync(command, args, {
encoding: "utf8",
maxBuffer: 1024 * 1024,
timeout: 60_000,
...options,
});
if (result.status !== 0) throw new Error(`${command} failed during the live test`);
return result.stdout;
}

function waitForStart(child) {
return new Promise((resolve, reject) => {
let buffered = "";
const timeout = setTimeout(() => reject(new Error("egressd startup timed out")), 10_000);
const fail = () => {
clearTimeout(timeout);
reject(new Error("egressd exited before startup"));
};
child.once("error", fail);
child.once("exit", fail);
child.stderr.resume();
child.stdout.on("data", (chunk) => {
buffered += chunk;
const lines = buffered.split("\n");
buffered = lines.pop() ?? "";
for (const line of lines) {
if (!line.includes('"egressd.started"')) continue;
try {
const address = JSON.parse(line);
if (!Number.isInteger(address.port)) throw new Error("invalid port");
clearTimeout(timeout);
child.off("error", fail);
child.off("exit", fail);
resolve(address);
} catch {
clearTimeout(timeout);
reject(new Error("egressd emitted an invalid startup event"));
}
return;
}
});
});
}

async function stopDaemon(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) return;
const exited = new Promise((resolve) => child.once("exit", resolve));
if (child.exitCode !== null || child.signalCode !== null) return;
if (!child.kill("SIGTERM")) return;
const stopped = await Promise.race([
exited.then(() => true),
new Promise((resolve) => setTimeout(() => resolve(false), 3_000)),
]);
if (!stopped && child.exitCode === null) {
child.kill("SIGKILL");
await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 1_000))]);
}
}

function parseObservedIp(output) {
try {
const value = JSON.parse(output).ip;
return typeof value === "string" ? value : undefined;
} catch {
throw new Error("target returned invalid JSON");
}
}

export async function main() {
const subscriptionUrl = required("EGRESSKIT_LIVE_SUBSCRIPTION_URL");
const targetUrl = validateTargetUrl(required("EGRESSKIT_LIVE_TARGET_URL")).href;
const mihomoBinary = required("EGRESSKIT_MIHOMO_BINARY");
const stateDirectory = await mkdtemp(join(tmpdir(), "egresskit-live-vless-"));
const adminToken = randomBytes(24).toString("hex");
const proxyToken = randomBytes(24).toString("hex");
const childEnvironment = cleanChildEnvironment(process.env);
let daemon;
let cancelled = false;
const interrupt = () => {
cancelled = true;
daemon?.kill("SIGTERM");
};
const ensureNotCancelled = () => {
if (cancelled) throw new Error("live test interrupted");
};
process.once("SIGINT", interrupt);
process.once("SIGTERM", interrupt);

try {
run("pnpm", ["--filter", "@egresskit/egressd", "build"], { env: childEnvironment });
ensureNotCancelled();
daemon = spawn("node", ["apps/egressd/dist/cli.js"], {
env: {
...childEnvironment,
EGRESSKIT_ADMIN_TOKEN: adminToken,
EGRESSKIT_MIHOMO_BINARY: mihomoBinary,
EGRESSKIT_PORT: "0",
EGRESSKIT_PROXY_TOKEN: proxyToken,
EGRESSKIT_STATE_DIRECTORY: stateDirectory,
},
stdio: ["ignore", "pipe", "pipe"],
});
const address = await waitForStart(daemon);
ensureNotCancelled();
run("node", ["apps/egressd/dist/control-cli-bin.js", "subscription", "add", "--redact"], {
env: {
...childEnvironment,
EGRESSKIT_ADMIN_TOKEN: adminToken,
EGRESSKIT_STATE_DIRECTORY: stateDirectory,
},
input: subscriptionUrl,
});
ensureNotCancelled();
const curlBase = ["--fail", "--silent", "--show-error", "--max-time", "30"];
const direct = parseObservedIp(
run("curl", [...curlBase, targetUrl], { env: childEnvironment }),
);
ensureNotCancelled();
const observed = parseObservedIp(
run(
"curl",
[
...curlBase,
"--noproxy",
"",
"--proxy",
`http://127.0.0.1:${address.port}`,
"--proxy-user",
`rotate:${proxyToken}`,
targetUrl,
],
{ env: childEnvironment },
),
);
ensureNotCancelled();
if (!isVerifiedExit(direct, observed)) {
throw new Error("the target did not observe a distinct valid proxy exit");
}
process.stdout.write(
`${JSON.stringify({ localImplementation: "passed", mihomoAcceptance: "passed", targetReachability: "passed", realExitVerification: "passed" })}\n`,
);
} finally {
process.off("SIGINT", interrupt);
process.off("SIGTERM", interrupt);
try {
await stopDaemon(daemon);
} finally {
await rm(stateDirectory, { force: true, recursive: true });
}
}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : "live test failed"}\n`);
process.exitCode = 1;
});
}
25 changes: 25 additions & 0 deletions scripts/live-vless.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import test from "node:test";

import { cleanChildEnvironment, isVerifiedExit, validateTargetUrl } from "./live-vless.mjs";

test("live target must be HTTPS and both observed values must be distinct IP addresses", () => {
assert.throws(() => validateTargetUrl("http://target.example/json"), /HTTPS/);
assert.equal(validateTargetUrl("https://target.example/json").protocol, "https:");
assert.equal(isVerifiedExit("direct", "proxy"), false);
assert.equal(isVerifiedExit("192.0.2.1", "192.0.2.1"), false);
assert.equal(isVerifiedExit("192.0.2.1", "2001:db8::1"), true);
});

test("live secrets are not inherited by daemon, CLI, Mihomo, or curl", () => {
assert.deepEqual(
cleanChildEnvironment({
EGRESSKIT_LIVE_SUBSCRIPTION_URL: "secret",
EGRESSKIT_LIVE_TARGET_URL: "https://target.example",
EGRESSKIT_MIHOMO_HTTP_LISTENER: "http://127.0.0.1:9999",
EGRESSKIT_CONTROL_SOCKET: "/wrong/socket",
PATH: "/bin",
}),
{ PATH: "/bin" },
);
});
Loading