Spaces:
Runtime error
Runtime error
File size: 4,360 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | /**
* Fase 3 / Epic A — TPROXY capture-mode manager (decrypt 4a/N).
*
* Singleton lifecycle around the decrypt-capable transparent listener
* (`captureMode.ts`): 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 to drive.
*
* The OS trust-store mechanism is deliberately NOT baked in here: `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). This keeps the manager unit-testable without root and means
* it never mutates the trust store on its own.
*/
import { DynamicCertStore } from "./dynamicCert";
import { startTproxyCapture, type TproxyCaptureHandle } from "./captureMode";
import { isTransparentSocketAvailable } from "./transparentSocket";
import type { TproxyConfig } from "./commands";
export interface CaptureManagerStatus {
/** Whether a capture session is currently running. */
running: boolean;
/** Whether the native IP_TRANSPARENT addon is loadable on this host. */
available: boolean;
/** ISO timestamp of when the running session started. */
startedAt?: string;
/** Number of connections intercepted in the running session. */
interceptCount?: number;
/** The transparent listener port of the running session. */
onPort?: number;
}
export interface CaptureManagerDeps {
startTproxyCapture: typeof startTproxyCapture;
isAvailable: () => boolean;
createCertStore: () => DynamicCertStore;
now: () => string;
}
const realDeps: CaptureManagerDeps = {
startTproxyCapture,
isAvailable: isTransparentSocketAvailable,
createCertStore: () => new DynamicCertStore(),
now: () => new Date().toISOString(),
};
export interface StartCaptureModeOptions {
cfg: TproxyConfig;
/** Install the dynamic CA cert (PEM) into the OS trust store. Injected so the
* manager never touches the trust store itself. */
installCa: (caPem: string) => Promise<void>;
/** Remove the CA from the trust store on stop (symmetric teardown). Injected. */
uninstallCa: () => Promise<void>;
/** Seam overrides for unit testing. */
deps?: Partial<CaptureManagerDeps>;
}
interface ActiveCapture {
handle: TproxyCaptureHandle;
startedAt: string;
intercepts: { count: number };
}
let active: ActiveCapture | null = null;
/**
* Start the decrypt-capable TPROXY capture mode. Rejects if a session is already
* running or the native addon is unavailable. The dynamic CA is created here and
* installed in the trust store via the injected `installCa`.
*/
export async function startCaptureMode(
options: StartCaptureModeOptions
): Promise<CaptureManagerStatus> {
if (active) throw new Error("TPROXY capture mode is already running");
const deps: CaptureManagerDeps = { ...realDeps, ...options.deps };
if (!deps.isAvailable()) {
throw new Error("TPROXY capture mode requires the native addon (Linux + CAP_NET_ADMIN).");
}
const certStore = deps.createCertStore();
const intercepts = { count: 0 };
const handle = await deps.startTproxyCapture(options.cfg, {
decrypt: { certStore, installCa: options.installCa, uninstallCa: options.uninstallCa },
onIntercept: () => {
intercepts.count += 1;
},
});
active = { handle, startedAt: deps.now(), intercepts };
return getCaptureStatus();
}
/** Stop the running capture session (closes the listener, uninstalls the CA, and
* reverts the rules via the handle). Idempotent — a no-op when nothing runs. */
export async function stopCaptureMode(): Promise<CaptureManagerStatus> {
const current = active;
active = null;
if (current) await current.handle.stop();
return getCaptureStatus();
}
/** Current capture-mode status (safe to call any time, including when idle). */
export function getCaptureStatus(): CaptureManagerStatus {
const available = isTransparentSocketAvailable();
if (!active) return { running: false, available };
return {
running: true,
available,
startedAt: active.startedAt,
interceptCount: active.intercepts.count,
onPort: active.handle.cfg.onPort,
};
}
/** Test-only: clear the singleton without invoking teardown. */
export function __resetCaptureManager(): void {
active = null;
}
|