mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
OmniRoute v3.8.29 — 115 commits since v3.8.28. Full CHANGELOG + 41 i18n mirrors. All content quality gates green (build, unit 8/8, vitest 188/188, PR test policy, quality gates extended, docs sync, quality ratchet). Remaining red CI checks are pre-existing release flakes (coverage-shard/integration/node-compat teardown), a new transitive undici advisory in electron devDeps, and a workflow-level CodeQL fail (0 open alerts). VPS-validated by the operator.
132 lines
5.3 KiB
TypeScript
132 lines
5.3 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|