Spaces:
Runtime error
Runtime error
| /** | |
| * Fase 3 / Epic A β TPROXY transparent capture mode (Linux): command builder. | |
| * | |
| * A 5th capture mode that intercepts TCP transparently via Linux TPROXY + policy | |
| * routing, WITHOUT spoofing `/etc/hosts` or mutating OS-wide system-proxy | |
| * settings (headless-friendly, auto-flushed on reboot). | |
| * | |
| * β οΈ OUTPUT-based recipe (validated e2e on the VPS, kernel 6.8.0): the MITM use | |
| * case is LOCAL outbound traffic (IDE agents on the same host), which TPROXY in | |
| * PREROUTING does NOT see β PREROUTING only sees *forwarded* traffic. So we mark | |
| * new local outbound connections in the `mangle OUTPUT` chain, an `ip rule` | |
| * reroutes the marked packets to local delivery (`lo`), and on re-entry the | |
| * `mangle PREROUTING` TPROXY target assigns them to the IP_TRANSPARENT listener. | |
| * (An earlier PREROUTING-only recipe was proven not to intercept local traffic.) | |
| * | |
| * Validated recipe (test port 9999, fwmark 0x2333 β isolated from prod 443/80): | |
| * iptables -t mangle -A OUTPUT -p tcp --dport N [-m mark ! --mark BYPASS] -j MARK --set-mark M | |
| * ip rule add fwmark M lookup T | |
| * ip route add local 0.0.0.0/0 dev lo table T | |
| * iptables -t mangle -A PREROUTING -p tcp --dport N -m mark --mark M -j TPROXY --on-port L --tproxy-mark M | |
| * Result: client CONNECTED, listener saw orig-dest preserved (198.51.100.7:9999). | |
| * | |
| * Pure + unit-testable: the exact `iptables`/`ip` commands for apply and revert, | |
| * with the invariant that **revert is the precise inverse of apply, in reverse | |
| * order** β a crash must never leave a mangle rule behind (the Fase 1 / | |
| * `repairMitm()` invariant). `setup.ts` runs these via `execFile` (arrays, never | |
| * a shell string β Hard Rule #13). | |
| * | |
| * `bypassMark` (anti-loop): the SO_MARK the proxy sets on its OWN upstream | |
| * connections; the OUTPUT rule excludes it so the proxy's forwarded traffic is | |
| * not re-intercepted (infinite loop). When omitted, no exclusion is emitted | |
| * (fine for a metadata-only listener that never forwards). | |
| */ | |
| export interface TproxyConfig { | |
| /** Destination TCP port to transparently intercept (e.g. 443). */ | |
| dport: number; | |
| /** Firewall mark set on OUTPUT and matched by the ip rule + PREROUTING (e.g. 0x2333). */ | |
| mark: number; | |
| /** Local port the IP_TRANSPARENT listener binds (e.g. 8443). */ | |
| onPort: number; | |
| /** Policy-routing table id holding the `local 0.0.0.0/0` route (e.g. 233). */ | |
| routeTable: number; | |
| /** SO_MARK the proxy sets on its own upstream conns; excluded in OUTPUT (anti-loop). */ | |
| bypassMark?: number; | |
| } | |
| /** A single command to run via `execFile(bin, args)` β never a shell string. */ | |
| export interface TproxyCommand { | |
| bin: string; | |
| args: string[]; | |
| } | |
| function isPort(n: number): boolean { | |
| return Number.isInteger(n) && n >= 1 && n <= 65535; | |
| } | |
| /** | |
| * Validate a config before any command is built/run. Returns an error message | |
| * string, or null when the config is sane. | |
| */ | |
| export function validateTproxyConfig(cfg: TproxyConfig): string | null { | |
| if (!isPort(cfg.dport)) return `dport must be a valid TCP port (1-65535), got ${cfg.dport}`; | |
| if (!isPort(cfg.onPort)) return `onPort must be a valid TCP port (1-65535), got ${cfg.onPort}`; | |
| if (!Number.isInteger(cfg.mark) || cfg.mark < 1) return `mark must be a positive integer, got ${cfg.mark}`; | |
| if (!Number.isInteger(cfg.routeTable) || cfg.routeTable < 1) { | |
| return `routeTable must be a positive integer, got ${cfg.routeTable}`; | |
| } | |
| if (cfg.bypassMark !== undefined) { | |
| if (!Number.isInteger(cfg.bypassMark) || cfg.bypassMark < 1) { | |
| return `bypassMark must be a positive integer when set, got ${cfg.bypassMark}`; | |
| } | |
| if (cfg.bypassMark === cfg.mark) return "bypassMark must differ from mark (anti-loop)"; | |
| } | |
| return null; | |
| } | |
| /** OUTPUT mangle rule spec (mark new local outbound conns), shared so -A/-D match. */ | |
| function outputRuleSpec(cfg: TproxyConfig): string[] { | |
| const spec = ["-t", "mangle", "OUTPUT", "-p", "tcp", "--dport", String(cfg.dport)]; | |
| if (cfg.bypassMark !== undefined) { | |
| spec.push("-m", "mark", "!", "--mark", String(cfg.bypassMark)); | |
| } | |
| spec.push("-j", "MARK", "--set-mark", String(cfg.mark)); | |
| return spec; | |
| } | |
| /** PREROUTING mangle TPROXY rule spec (assign marked, rerouted packets to the listener). */ | |
| function preroutingRuleSpec(cfg: TproxyConfig): string[] { | |
| return [ | |
| "-t", "mangle", "PREROUTING", | |
| "-p", "tcp", "--dport", String(cfg.dport), | |
| "-m", "mark", "--mark", String(cfg.mark), | |
| "-j", "TPROXY", "--on-port", String(cfg.onPort), "--tproxy-mark", String(cfg.mark), | |
| ]; | |
| } | |
| /** Build an iptables command from a `[-t table, CHAIN, ...rest]` spec + the op flag. */ | |
| function iptables(op: "-A" | "-D", spec: string[]): TproxyCommand { | |
| const [t, table, chain, ...rest] = spec; | |
| return { bin: "iptables", args: [t, table, op, chain, ...rest] }; | |
| } | |
| /** Commands to enable TPROXY interception of local outbound traffic, in apply order. */ | |
| export function buildTproxyApplyCommands(cfg: TproxyConfig): TproxyCommand[] { | |
| return [ | |
| { bin: "ip", args: ["rule", "add", "fwmark", String(cfg.mark), "lookup", String(cfg.routeTable)] }, | |
| { bin: "ip", args: ["route", "add", "local", "0.0.0.0/0", "dev", "lo", "table", String(cfg.routeTable)] }, | |
| iptables("-A", outputRuleSpec(cfg)), | |
| iptables("-A", preroutingRuleSpec(cfg)), | |
| ]; | |
| } | |
| /** Commands to undo TPROXY interception β exact inverse of apply, reverse order. */ | |
| export function buildTproxyRevertCommands(cfg: TproxyConfig): TproxyCommand[] { | |
| return [ | |
| iptables("-D", preroutingRuleSpec(cfg)), | |
| iptables("-D", outputRuleSpec(cfg)), | |
| { bin: "ip", args: ["route", "del", "local", "0.0.0.0/0", "dev", "lo", "table", String(cfg.routeTable)] }, | |
| { bin: "ip", args: ["rule", "del", "fwmark", String(cfg.mark), "lookup", String(cfg.routeTable)] }, | |
| ]; | |
| } | |