mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
resolveSudoSpawn() drops the `sudo -S` prefix when already root, when sudo is not installed (slim containers) or under OMNIROUTE_NO_SUDO (#6122), so the spawned command is `tee` rather than `sudo` in those environments. The three addDNSEntries assertions hardcoded `sudo` and failed whenever the suite ran as root. Assert the effective invocation (tee -a <hosts file>) instead, still checking the -S password flag when elevation is actually in play. Proof: with OMNIROUTE_NO_SUDO=1 the file went 3 failing -> 8 passing; the unelevated (sudo) path stays 8 passing.
224 lines
8.0 KiB
TypeScript
224 lines
8.0 KiB
TypeScript
/**
|
|
* Unit tests: OMNIROUTE_SKIP_DNS_WRITE guard on addDNSEntries / removeDNSEntries.
|
|
*
|
|
* A loader hook replaces child_process.spawn with a mock that records calls
|
|
* instead of executing real sudo. The mock is process-wide but only replaces
|
|
* spawn — execFile/execFileSync remain real (used by isSudoAvailable which
|
|
* is harmless).
|
|
*
|
|
* The isolateDataDir.ts setup (loaded via --import) sets
|
|
* OMNIROUTE_SKIP_DNS_WRITE=1 by default, so the guard tests are inherently
|
|
* safe. The "proceeds" tests temporarily clear the guard and rely on the
|
|
* spawn mock to prevent real sudo.
|
|
*
|
|
* removeDNSEntries "proceeds" tests use a sentinel host (__dns_guard_test__).
|
|
* When the host is present in /etc/hosts (injected by _run_dns_guard_test.sh
|
|
* before the test), the full exec path is exercised and spawn calls are
|
|
* asserted. When absent, the function returns early at the presentHosts check
|
|
* and we verify zero spawn calls — still proving the guard did not block.
|
|
* This makes the test self-contained regardless of /etc/hosts content.
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import { register } from "node:module";
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// Register loader hook — must happen before any dnsConfig.ts import.
|
|
register(new URL("../_cp_mock_hook.mts", import.meta.url).href, import.meta.url);
|
|
|
|
// The mock module's spawn call log.
|
|
const { spawnCalls, resetSpawnCalls } = await import("../_cp_mock_module.mts");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let importCounter = 0;
|
|
|
|
/** Sentinel hostname for removeDNSEntries exec-path tests. */
|
|
const RM_TEST_HOST = "__dns_guard_test__";
|
|
|
|
/** True when RM_TEST_HOST has IPv4+IPv6 entries in /etc/hosts. */
|
|
let hostIsPresent = false;
|
|
try {
|
|
const hostsContent = fs.readFileSync("/etc/hosts", "utf8");
|
|
const lines = hostsContent.split(/\r?\n/);
|
|
hostIsPresent = [`127.0.0.1 ${RM_TEST_HOST}`, `::1 ${RM_TEST_HOST}`].every(
|
|
(entry) => {
|
|
const [ip, host] = entry.split(/\s+/);
|
|
return lines.some((line) => {
|
|
const parts = line.trim().split(/\s+/).filter(Boolean);
|
|
return parts.length >= 2 && parts[0] === ip && parts.includes(host);
|
|
});
|
|
},
|
|
);
|
|
} catch {
|
|
// /etc/hosts not readable — treat as absent.
|
|
}
|
|
|
|
/**
|
|
* Assert the spawn that writes /etc/hosts.
|
|
*
|
|
* `resolveSudoSpawn()` (src/mitm/systemCommands.ts) deliberately drops the
|
|
* `sudo -S` prefix when the process is already root, when `sudo` is not
|
|
* installed (slim containers) or under `OMNIROUTE_NO_SUDO` (#6122) — so the
|
|
* spawned command is `sudo` for an unprivileged user and the bare underlying
|
|
* binary otherwise. Hardcoding `sudo` made this test fail whenever the suite
|
|
* ran as root. Assert the *effective* invocation instead: the write always
|
|
* goes through `tee -a <hosts file>`, elevated or not.
|
|
*/
|
|
function assertHostsWriteSpawn(call: { command: string; args: string[] }): void {
|
|
if (process.platform === "win32") {
|
|
assert.equal(call.command, "powershell.exe", "should invoke powershell.exe");
|
|
return;
|
|
}
|
|
const argv = [call.command, ...call.args];
|
|
assert.ok(argv.includes("tee"), `should write hosts via tee (got: ${argv.join(" ")})`);
|
|
assert.ok(argv.includes("-a"), `tee should append, not truncate (got: ${argv.join(" ")})`);
|
|
if (call.command === "sudo") {
|
|
assert.ok(call.args.includes("-S"), "sudo should use -S flag for password stdin");
|
|
} else {
|
|
assert.equal(call.command, "tee", `unelevated write should invoke tee directly (got ${call.command})`);
|
|
}
|
|
}
|
|
|
|
function guardEnv(value: string | undefined): () => void {
|
|
const prev = process.env.OMNIROUTE_SKIP_DNS_WRITE;
|
|
if (value === undefined) {
|
|
delete process.env.OMNIROUTE_SKIP_DNS_WRITE;
|
|
} else {
|
|
process.env.OMNIROUTE_SKIP_DNS_WRITE = value;
|
|
}
|
|
return () => {
|
|
if (prev === undefined) delete process.env.OMNIROUTE_SKIP_DNS_WRITE;
|
|
else process.env.OMNIROUTE_SKIP_DNS_WRITE = prev;
|
|
};
|
|
}
|
|
|
|
// Unique import URL per test to bypass ESM module cache.
|
|
async function importDnsConfig() {
|
|
return import(`../../src/mitm/dns/dnsConfig.ts?t=${++importCounter}`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// addDNSEntries guard
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("addDNSEntries: returns early when OMNIROUTE_SKIP_DNS_WRITE=1", async () => {
|
|
resetSpawnCalls();
|
|
const { addDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv("1");
|
|
try {
|
|
await addDNSEntries(["test-host.example.com"], "fake-pw");
|
|
assert.equal(spawnCalls.length, 0, "spawn should NOT be called when guard is active");
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test("addDNSEntries: proceeds when env var is unset", async () => {
|
|
resetSpawnCalls();
|
|
const { addDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv(undefined);
|
|
try {
|
|
await addDNSEntries(["__test_add_unset__.example.com"], "fake-pw");
|
|
assert.ok(spawnCalls.length > 0, "spawn should be called when guard is off");
|
|
assertHostsWriteSpawn(spawnCalls[0]);
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test("addDNSEntries: proceeds when OMNIROUTE_SKIP_DNS_WRITE=0", async () => {
|
|
resetSpawnCalls();
|
|
const { addDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv("0");
|
|
try {
|
|
await addDNSEntries(["__test_add_0__.example.com"], "fake-pw");
|
|
assert.ok(spawnCalls.length > 0, "spawn should be called for value '0'");
|
|
assertHostsWriteSpawn(spawnCalls[0]);
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test("addDNSEntries: guard does NOT trigger for value 'true'", async () => {
|
|
resetSpawnCalls();
|
|
const { addDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv("true");
|
|
try {
|
|
await addDNSEntries(["__test_add_true__.example.com"], "fake-pw");
|
|
assert.ok(spawnCalls.length > 0, "spawn should be called for value 'true'");
|
|
assertHostsWriteSpawn(spawnCalls[0]);
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// removeDNSEntries guard
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("removeDNSEntries: returns early when OMNIROUTE_SKIP_DNS_WRITE=1", async () => {
|
|
resetSpawnCalls();
|
|
const { removeDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv("1");
|
|
try {
|
|
await removeDNSEntries([RM_TEST_HOST], "fake-pw");
|
|
assert.equal(spawnCalls.length, 0, "spawn should NOT be called when guard is active");
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test("removeDNSEntries: proceeds when env var is unset", async () => {
|
|
resetSpawnCalls();
|
|
const { removeDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv(undefined);
|
|
try {
|
|
await removeDNSEntries([RM_TEST_HOST], "fake-pw");
|
|
if (hostIsPresent) {
|
|
// Full exec path: host present → execFileWithPassword → mock spawn.
|
|
assert.ok(spawnCalls.length > 0, "spawn called for present host");
|
|
} else {
|
|
// Host absent → presentHosts empty → early return, no spawn.
|
|
assert.equal(spawnCalls.length, 0, "no spawn for absent host");
|
|
}
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test("removeDNSEntries: proceeds when OMNIROUTE_SKIP_DNS_WRITE=0", async () => {
|
|
resetSpawnCalls();
|
|
const { removeDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv("0");
|
|
try {
|
|
await removeDNSEntries([RM_TEST_HOST], "fake-pw");
|
|
if (hostIsPresent) {
|
|
assert.ok(spawnCalls.length > 0, "spawn called for present host");
|
|
} else {
|
|
assert.equal(spawnCalls.length, 0, "no spawn for absent host");
|
|
}
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test("removeDNSEntries: guard does NOT trigger for value 'true'", async () => {
|
|
resetSpawnCalls();
|
|
const { removeDNSEntries } = await importDnsConfig();
|
|
const restore = guardEnv("true");
|
|
try {
|
|
await removeDNSEntries([RM_TEST_HOST], "fake-pw");
|
|
if (hostIsPresent) {
|
|
assert.ok(spawnCalls.length > 0, "spawn called for present host");
|
|
} else {
|
|
assert.equal(spawnCalls.length, 0, "no spawn for absent host");
|
|
}
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|