Spaces:
Runtime error
Runtime error
File size: 5,444 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 124 125 126 127 128 129 130 131 132 | /**
* Fase 3 / Epic A β loader for the TPROXY IP_TRANSPARENT native addon.
*
* Node's `net` module cannot `setsockopt(IP_TRANSPARENT)` before `bind()`, which
* TPROXY requires. `src/mitm/tproxy/native/transparent.c` is a tiny N-API addon
* that creates the transparent listening socket and returns its fd; Node adopts
* it via `server.listen({ fd })` and reads the original destination from
* `socket.localAddress`/`localPort` (TPROXY preserves it β no SO_ORIGINAL_DST).
*
* The addon is **optional**: it is Linux-only and must be built with a native
* toolchain (`npm run build:native:tproxy`) or shipped as a prebuild. This
* loader degrades gracefully so a JS-only install (no toolchain, or a non-Linux
* host) keeps working β the TPROXY capture mode is simply gated on availability.
*
* Viability proven on the VPS (kernel 6.8.0): the prebuilt `.node` loaded under
* a different Node version (N-API ABI-stable) and, as root, created the
* IP_TRANSPARENT socket which Node adopted.
*/
import { createRequire } from "node:module";
import { platform } from "node:os";
import path from "node:path";
export interface TransparentAddon {
/** socket()+SO_REUSEADDR+IP_TRANSPARENT+bind()+listen(); returns the raw fd. */
createTransparentListener(ip: string, port: number): number;
/** setsockopt(SO_MARK) on an fd β anti-loop: mark the proxy's own upstream conns. */
setSocketMark(fd: number, mark: number): void;
/** socket()+SO_MARK+non-blocking connect(); returns fd. Anti-loop forward path. */
connectMarked(ip: string, port: number, mark: number): number;
}
/** Path of the built/prebuilt addon, relative to `src/mitm/tproxy/`. */
const ADDON_REL_PATHS = [
"native/build/Release/transparent.node",
"native/prebuilds/transparent.node",
];
/**
* Candidate require() specifiers, in priority order:
* - module-relative (`./native/...`) β dev / source runs where this file sits
* next to `native/`;
* - cwd-absolute (`<cwd>/src/mitm/tproxy/native/...`) β the standalone/Docker
* bundle, where this module is compiled into `.next/server/...` (so the
* module-relative path misses) but `assembleStandalone` copies the addon to
* `<standalone-root>/src/mitm/tproxy/native/...` and the server runs with
* cwd = the standalone root.
*/
function addonCandidates(cwd: string): string[] {
return [
...ADDON_REL_PATHS.map((p) => `./${p}`),
...ADDON_REL_PATHS.map((p) => path.join(cwd, "src", "mitm", "tproxy", p)),
];
}
/**
* Attempt to load the native addon. Returns null (never throws) when the host is
* non-Linux or the addon hasn't been built. `req`/`os`/`cwd` are injectable for tests.
*/
export function loadTransparentAddon(
req: (path: string) => unknown = createRequire(import.meta.url),
os: () => string = platform,
cwd: () => string = () => process.cwd()
): TransparentAddon | null {
if (os() !== "linux") return null; // IP_TRANSPARENT is a Linux-only socket option
for (const candidate of addonCandidates(cwd())) {
try {
const mod = req(candidate) as Partial<TransparentAddon> | undefined;
if (
mod &&
typeof mod.createTransparentListener === "function" &&
typeof mod.setSocketMark === "function" &&
typeof mod.connectMarked === "function"
) {
return mod as TransparentAddon;
}
} catch {
// not built / not present at this path β try the next.
}
}
return null;
}
const cached: TransparentAddon | null = loadTransparentAddon();
/** True when the TPROXY transparent-socket addon is loadable on this host. */
export function isTransparentSocketAvailable(): boolean {
return cached !== null;
}
/**
* Create an IP_TRANSPARENT listening socket and return its fd for Node to adopt
* via `server.listen({ fd })`. Throws a clear, actionable error when the addon
* is unavailable (callers should check `isTransparentSocketAvailable()` first
* and disable the TPROXY capture mode).
*/
export function createTransparentListenerFd(ip: string, port: number): number {
if (!cached) {
throw new Error(
"TPROXY transparent-socket addon is not available. It is Linux-only and must be built " +
"(`npm run build:native:tproxy`, needs a C toolchain) or shipped as a prebuild; " +
"CAP_NET_ADMIN is required at runtime."
);
}
return cached.createTransparentListener(ip, port);
}
/**
* Set SO_MARK on a socket fd (anti-loop). The TPROXY listener marks its OWN
* upstream connections with the bypass mark so the mangle OUTPUT rule excludes
* them and they are not re-intercepted. Throws when the addon is unavailable.
*/
export function setSocketMark(fd: number, mark: number): void {
if (!cached) {
throw new Error("TPROXY transparent-socket addon is not available (setSocketMark).");
}
cached.setSocketMark(fd, mark);
}
/**
* Create a socket with SO_MARK set BEFORE a non-blocking connect (so the SYN
* carries the mark) and return its fd for Node to adopt via `new net.Socket({
* fd })`. This is the forward-path anti-loop: the proxy's upstream SYN is
* excluded by the OUTPUT rule, so the forward does not re-enter TPROXY. Throws
* when the addon is unavailable.
*/
export function connectMarked(ip: string, port: number, mark: number): number {
if (!cached) {
throw new Error("TPROXY transparent-socket addon is not available (connectMarked).");
}
return cached.connectMarked(ip, port, mark);
}
|