mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
Singleton lifecycle around the decrypt-capable transparent listener (#4200): builds the dynamic CA (#4173), gates on native-addon availability, holds the single running handle, counts interceptions, and exposes start / stop / status for a local-only route (4b) to drive. The OS trust-store mechanism is deliberately not baked in: installCa / uninstallCa are injected by the caller (the route / VPS e2e provides the real installer, which needs its own trust-store slot so it does not clobber the static MITM cert). The manager therefore never mutates the trust store itself and stays unit-testable without root or the native addon. - startCaptureMode: rejects when unavailable or already running; builds the CA; wires the injected installCa/uninstallCa through to the decrypt listener. - stopCaptureMode: stops the handle (close + uninstall CA + revert), idempotent. - getCaptureStatus: running / available / startedAt / interceptCount / onPort. Route + real trust-store installer + Traffic Inspector UI tab + VPS e2e-with-decrypt + addon prebuilds follow (4b+).
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
/**
|
|
* Fase 3 / Epic A — TPROXY capture-mode manager (decrypt 4a/N).
|
|
*
|
|
* The manager is the singleton lifecycle a (local-only) route drives: it gates on
|
|
* native-addon availability, builds the dynamic CA, wires the injected trust-store
|
|
* installer through to the decrypt listener, counts interceptions, and reports
|
|
* status. All effectful seams are injected so this is testable without root or the
|
|
* native addon. The real intercept/decrypt path was proven in #4169/#4179/#4200.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
startCaptureMode,
|
|
stopCaptureMode,
|
|
getCaptureStatus,
|
|
__resetCaptureManager,
|
|
type StartCaptureModeOptions,
|
|
} from "../../src/mitm/tproxy/captureManager.ts";
|
|
|
|
const CFG = { dport: 443, mark: 0x2333, onPort: 8443, routeTable: 233, bypassMark: 0x539 };
|
|
|
|
interface CapturedStart {
|
|
cfg?: unknown;
|
|
options?: {
|
|
decrypt?: { installCa: (p: string) => Promise<void>; uninstallCa: () => Promise<void> };
|
|
onIntercept?: (info: { destIp: string; destPort: number }) => void;
|
|
};
|
|
stopped: boolean;
|
|
}
|
|
|
|
/** A fake `startTproxyCapture` that records what it was handed and returns a
|
|
* handle whose stop() flips a flag. */
|
|
function fakeStart(rec: CapturedStart) {
|
|
return async (cfg: unknown, options: CapturedStart["options"]) => {
|
|
rec.cfg = cfg;
|
|
rec.options = options;
|
|
return {
|
|
cfg: cfg as never,
|
|
server: {} as never,
|
|
stop: async () => {
|
|
rec.stopped = true;
|
|
},
|
|
};
|
|
};
|
|
}
|
|
|
|
function baseOptions(over: Partial<StartCaptureModeOptions> = {}): StartCaptureModeOptions {
|
|
return {
|
|
cfg: CFG,
|
|
installCa: async () => {},
|
|
uninstallCa: async () => {},
|
|
...over,
|
|
};
|
|
}
|
|
|
|
test.afterEach(() => __resetCaptureManager());
|
|
|
|
test("startCaptureMode rejects when the native addon is unavailable", async () => {
|
|
await assert.rejects(
|
|
() => startCaptureMode(baseOptions({ deps: { isAvailable: () => false } })),
|
|
/native addon|CAP_NET_ADMIN/
|
|
);
|
|
});
|
|
|
|
test("startCaptureMode wires the injected CA installer into the decrypt listener", async () => {
|
|
const rec: CapturedStart = { stopped: false };
|
|
const installCa = async () => {};
|
|
const uninstallCa = async () => {};
|
|
const status = await startCaptureMode(
|
|
baseOptions({
|
|
installCa,
|
|
uninstallCa,
|
|
deps: { isAvailable: () => true, startTproxyCapture: fakeStart(rec) as never },
|
|
})
|
|
);
|
|
assert.equal(status.running, true);
|
|
assert.equal(status.onPort, 8443);
|
|
assert.equal(rec.options?.decrypt?.installCa, installCa, "installCa is passed through to decrypt");
|
|
assert.equal(rec.options?.decrypt?.uninstallCa, uninstallCa, "uninstallCa is passed through");
|
|
});
|
|
|
|
test("startCaptureMode refuses to start a second concurrent session", async () => {
|
|
const rec: CapturedStart = { stopped: false };
|
|
const deps = { isAvailable: () => true, startTproxyCapture: fakeStart(rec) as never };
|
|
await startCaptureMode(baseOptions({ deps }));
|
|
await assert.rejects(() => startCaptureMode(baseOptions({ deps })), /already running/);
|
|
});
|
|
|
|
test("stopCaptureMode stops the handle and clears the running state", async () => {
|
|
const rec: CapturedStart = { stopped: false };
|
|
await startCaptureMode(
|
|
baseOptions({ deps: { isAvailable: () => true, startTproxyCapture: fakeStart(rec) as never } })
|
|
);
|
|
const status = await stopCaptureMode();
|
|
assert.equal(rec.stopped, true, "the handle's stop() was invoked");
|
|
assert.equal(status.running, false);
|
|
});
|
|
|
|
test("stopCaptureMode is a no-op when nothing is running", async () => {
|
|
const status = await stopCaptureMode();
|
|
assert.equal(status.running, false);
|
|
});
|
|
|
|
test("getCaptureStatus counts interceptions reported by the listener", async () => {
|
|
const rec: CapturedStart = { stopped: false };
|
|
await startCaptureMode(
|
|
baseOptions({
|
|
deps: { isAvailable: () => true, startTproxyCapture: fakeStart(rec) as never },
|
|
})
|
|
);
|
|
rec.options?.onIntercept?.({ destIp: "1.2.3.4", destPort: 443 });
|
|
rec.options?.onIntercept?.({ destIp: "5.6.7.8", destPort: 443 });
|
|
const status = getCaptureStatus();
|
|
assert.equal(status.running, true);
|
|
assert.equal(status.interceptCount, 2);
|
|
});
|
|
|
|
test("getCaptureStatus reports not-running when idle", () => {
|
|
const status = getCaptureStatus();
|
|
assert.equal(status.running, false);
|
|
assert.equal(typeof status.available, "boolean");
|
|
});
|