Files
2026-07-13 13:39:12 +08:00

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