71 lines
3.3 KiB
TypeScript
71 lines
3.3 KiB
TypeScript
/**
|
|
* Fase 3 / Epic A — TPROXY setup layer (execFile wiring over the command builder).
|
|
*
|
|
* The builder (commands.ts) was validated against a real kernel on the VPS
|
|
* (apply/revert accepted, exact-inverse, zero impact on non-targeted traffic —
|
|
* see PR #4139). This layer adds the transactional runner: apply runs the
|
|
* commands in order and, if any step fails mid-way, runs a best-effort full
|
|
* revert so a partial apply never leaves firewall/routing state behind (the
|
|
* Fase 1 / repairMitm crash-safe invariant). revert is itself idempotent /
|
|
* best-effort. The command runner is injected so this is unit-testable without
|
|
* root; the real runner uses execFile (Hard Rule #13 — args array, no shell).
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { applyTproxy, revertTproxy } = await import("../../src/mitm/tproxy/setup.ts");
|
|
|
|
const CFG = { dport: 443, mark: 1, onPort: 8443, routeTable: 100 };
|
|
|
|
function recorder(failOnIndex = -1) {
|
|
const calls: Array<{ bin: string; args: string[] }> = [];
|
|
const run = async (bin: string, args: string[]) => {
|
|
calls.push({ bin, args });
|
|
if (calls.length - 1 === failOnIndex) throw new Error(`boom at ${failOnIndex}`);
|
|
};
|
|
return { calls, run };
|
|
}
|
|
|
|
test("applyTproxy runs the 4 OUTPUT-based apply commands in order via the injected runner", async () => {
|
|
const r = recorder();
|
|
await applyTproxy(CFG, r.run);
|
|
assert.equal(r.calls.length, 4);
|
|
assert.deepEqual(r.calls[0], { bin: "ip", args: ["rule", "add", "fwmark", "1", "lookup", "100"] });
|
|
assert.equal(r.calls[1].args[0], "route");
|
|
assert.deepEqual(r.calls[2].args.slice(0, 4), ["-t", "mangle", "-A", "OUTPUT"]);
|
|
assert.deepEqual(r.calls[3].args.slice(0, 4), ["-t", "mangle", "-A", "PREROUTING"]);
|
|
});
|
|
|
|
test("applyTproxy rejects an invalid config before running anything", async () => {
|
|
const r = recorder();
|
|
await assert.rejects(() => applyTproxy({ ...CFG, dport: 0 }, r.run), /dport/i);
|
|
assert.equal(r.calls.length, 0, "no command runs when the config is invalid");
|
|
});
|
|
|
|
test("applyTproxy runs a best-effort full revert when a command fails mid-way", async () => {
|
|
const r = recorder(1); // fail on the 2nd apply command (ip route add)
|
|
await assert.rejects(() => applyTproxy(CFG, r.run), /boom/);
|
|
// apply[0], apply[1]=fail, then the 4 revert commands (best-effort cleanup) = 6
|
|
assert.equal(r.calls.length, 6);
|
|
// first revert command tears down the PREROUTING rule (reverse order)
|
|
assert.deepEqual(r.calls[2].args.slice(0, 4), ["-t", "mangle", "-D", "PREROUTING"]);
|
|
// last revert command removes the ip rule
|
|
assert.deepEqual(r.calls[5], { bin: "ip", args: ["rule", "del", "fwmark", "1", "lookup", "100"] });
|
|
});
|
|
|
|
test("revertTproxy runs all 4 reverts best-effort even if one fails (idempotent)", async () => {
|
|
const r = recorder(1); // 2nd revert throws (e.g. rule not present)
|
|
await revertTproxy(CFG, r.run); // must NOT throw
|
|
assert.equal(r.calls.length, 4, "all four reverts attempted despite the failure");
|
|
});
|
|
|
|
test("every command the runner receives has string args (execFile-safe)", async () => {
|
|
const r = recorder();
|
|
await applyTproxy(CFG, r.run);
|
|
await revertTproxy(CFG, r.run);
|
|
for (const c of r.calls) {
|
|
assert.ok(typeof c.bin === "string");
|
|
for (const a of c.args) assert.equal(typeof a, "string");
|
|
}
|
|
});
|