Spaces:
Runtime error
Runtime error
File size: 5,781 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 120 121 122 123 | /**
* 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)] },
];
}
|