mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-09 17:28:51 +00:00
Some checks are pending
Publish Fork Image to GHCR / Build and Push Fork Image (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Change Classification (push) Waiting to run
CI / Quality Ratchet (push) Blocked by required conditions
CI / Quality Gates (Extended) (push) Waiting to run
CI / Docs Sync (Strict) (push) Waiting to run
CI / Docs Lint (prose — advisory) (push) Waiting to run
CI / i18n UI Coverage (push) Waiting to run
CI / Build language matrix (push) Waiting to run
CI / i18n Validation (push) Blocked by required conditions
CI / PR Test Policy (push) Waiting to run
CI / Build (push) Blocked by required conditions
CI / Package Artifact (push) Blocked by required conditions
CI / Electron Package Smoke (push) Blocked by required conditions
CI / Unit Tests (1/8) (push) Blocked by required conditions
CI / Unit Tests (2/8) (push) Blocked by required conditions
CI / Unit Tests (3/8) (push) Blocked by required conditions
CI / Unit Tests (4/8) (push) Blocked by required conditions
CI / E2E Tests (3/9) (push) Blocked by required conditions
CI / Unit Tests (5/8) (push) Blocked by required conditions
CI / Unit Tests (6/8) (push) Blocked by required conditions
CI / Unit Tests (7/8) (push) Blocked by required conditions
CI / Unit Tests (8/8) (push) Blocked by required conditions
CI / Vitest (MCP / autoCombo / UI components) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (1/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (2/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (3/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (4/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Build (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (1/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (2/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (3/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (4/4) (push) Blocked by required conditions
CI / Coverage Shard (1/8) (push) Blocked by required conditions
CI / Coverage Shard (2/8) (push) Blocked by required conditions
CI / Coverage Shard (3/8) (push) Blocked by required conditions
CI / Coverage Shard (4/8) (push) Blocked by required conditions
CI / Coverage Shard (5/8) (push) Blocked by required conditions
CI / Coverage Shard (6/8) (push) Blocked by required conditions
CI / Coverage Shard (7/8) (push) Blocked by required conditions
CI / Coverage Shard (8/8) (push) Blocked by required conditions
CI / Coverage (push) Blocked by required conditions
CI / SonarQube (push) Blocked by required conditions
CI / PR Coverage Comment (push) Blocked by required conditions
CI / E2E Tests (1/9) (push) Blocked by required conditions
CI / E2E Tests (2/9) (push) Blocked by required conditions
CI / E2E Tests (4/9) (push) Blocked by required conditions
CI / E2E Tests (5/9) (push) Blocked by required conditions
CI / E2E Tests (6/9) (push) Blocked by required conditions
CI / E2E Tests (7/9) (push) Blocked by required conditions
CI / E2E Tests (8/9) (push) Blocked by required conditions
CI / E2E Tests (9/9) (push) Blocked by required conditions
CI / Integration Tests (1/2) (push) Blocked by required conditions
CI / Integration Tests (2/2) (push) Blocked by required conditions
CI / Security Tests (push) Blocked by required conditions
CI / CI Dashboard (push) Blocked by required conditions
Publish to Docker Hub / Resolve Docker release metadata (push) Waiting to run
Publish to Docker Hub / Build Docker (linux/amd64) (push) Blocked by required conditions
Publish to Docker Hub / Build Docker (linux/arm64) (push) Blocked by required conditions
Publish to Docker Hub / Publish multi-arch manifests (push) Blocked by required conditions
opencode-plugin CI / Test (Node 24) (push) Waiting to run
opencode-plugin CI / Test (Node 22) (push) Waiting to run
opencode-plugin CI / Build (push) Blocked by required conditions
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
semgrep / semgrep (push) Waiting to run
Wiki Sync / Sync wiki with docs (push) Waiting to run
v3.8.40 cycle integration → main. All test gates green (Unit/Integration/Coverage/Node-compat/Quality-Ratchet). The only red check, 'PR Test Policy', is the test-masking heuristic firing on the cumulative ~57-commit release diff (legitimate assert consolidations already reviewed per-PR — Gemini CLI removal #5246, retired GPT models #5280, provider catalog refreshes); overridden with --admin per the documented release-PR convention. CodeQL/SonarQube advisory scans non-blocking; #5278's code already passed CodeQL on main. Homologated on VPS 192.168.0.15 (v3.8.40 healthy).
381 lines
26 KiB
Markdown
381 lines
26 KiB
Markdown
---
|
||
title: "MITM TPROXY Transparent Decrypt"
|
||
version: 3.8.40
|
||
lastUpdated: 2026-06-28
|
||
---
|
||
|
||
# MITM TPROXY Transparent Decrypt
|
||
|
||
TPROXY transparent decrypt is OmniRoute's **5th capture mode** for the
|
||
[Traffic Inspector](../frameworks/TRAFFIC_INSPECTOR.md) / [AgentBridge](../frameworks/AGENTBRIDGE.md)
|
||
MITM stack. It intercepts and **decrypts** local outbound HTTPS traffic on Linux
|
||
using kernel TPROXY + policy routing — **without** spoofing `/etc/hosts` and
|
||
**without** mutating OS-wide system-proxy settings. It is headless-friendly
|
||
(no DNS edits to clean up) and the firewall rules auto-flush on reboot.
|
||
|
||
Unlike the other capture modes, TPROXY needs no per-host setup: it transparently
|
||
intercepts **arbitrary** destination hosts on a target port, terminates TLS with
|
||
a leaf certificate it issues on the fly per SNI hostname, captures the decrypted
|
||
exchange, and re-encrypts the request to the original destination.
|
||
|
||
> **Linux-only, root-only, opt-in.** This mode requires Linux, a native addon
|
||
> built with a C toolchain, and the **CAP_NET_ADMIN** capability (typically root). It is gated
|
||
> behind the loopback-only AgentBridge API and disabled by default. A trusted
|
||
> MITM CA that can sign any host is a powerful capability — see [§6 Security](#6-security).
|
||
|
||
**Source:** `src/mitm/tproxy/`
|
||
**API route:** `GET / POST / DELETE /api/tools/agent-bridge/tproxy`
|
||
**Dashboard toggle:** Traffic Inspector → capture-modes toolbar → **"TPROXY Decrypt"** ⚠
|
||
**See also:** [`docs/frameworks/TRAFFIC_INSPECTOR.md`](../frameworks/TRAFFIC_INSPECTOR.md),
|
||
[`docs/frameworks/AGENTBRIDGE.md`](../frameworks/AGENTBRIDGE.md)
|
||
|
||
---
|
||
|
||
## §1 What it is and when to use it
|
||
|
||
The other four capture modes each have a limitation:
|
||
|
||
| Mode | How traffic is steered | Limitation |
|
||
| ----------------- | ------------------------------------------ | -------------------------------------- |
|
||
| AgentBridge | `/etc/hosts` DNS spoof of a fixed host set | only the registered IDE-agent hosts |
|
||
| Custom Hosts | `/etc/hosts` DNS spoof per host | one entry per host; sudo to edit hosts |
|
||
| HTTP_PROXY | `HTTP_PROXY`/`HTTPS_PROXY` env | only apps that honor the env var |
|
||
| System-wide proxy | OS proxy settings | mutates global state; needs revert |
|
||
|
||
TPROXY transparent decrypt steers traffic at the **kernel** layer instead. It
|
||
marks new local outbound TCP connections to a target port (default `443`) in the
|
||
`mangle OUTPUT` chain, an `ip rule` reroutes the marked packets to local delivery,
|
||
and on re-entry the `mangle PREROUTING` `TPROXY` target hands them to an
|
||
**IP_TRANSPARENT** listener — which then terminates TLS and captures the plaintext.
|
||
|
||
Use it when you want to capture and decrypt traffic from a process that:
|
||
|
||
- talks to a host AgentBridge does not register, and
|
||
- does not honor `HTTP_PROXY`, and
|
||
- you do not want to disturb with a system-wide proxy change.
|
||
|
||
Because interception happens in the kernel, the originating process needs **no
|
||
configuration change** — but the process must trust the dynamic CA OmniRoute
|
||
installs (see [§4](#4-the-per-sni-dynamic-ca-and-trust-store-installer)).
|
||
|
||
---
|
||
|
||
## §2 Requirements
|
||
|
||
| Requirement | Detail |
|
||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **OS** | Linux only — **IP_TRANSPARENT** is a Linux-only socket option. The loader returns "unavailable" on every other platform. |
|
||
| **Privilege** | The **CAP_NET_ADMIN** capability to create the transparent socket and apply `iptables`/`ip` rules — in practice, run as root. |
|
||
| **Native addon** | A tiny N-API addon (`src/mitm/tproxy/native/transparent.c`) must be built or shipped as a prebuild. See [§3](#3-the-native-ip_transparent-addon). |
|
||
| **Kernel modules** | `iptables` with the `TPROXY`, `mangle`, and `mark` match support (validated against kernel 6.8.0). |
|
||
|
||
**Graceful degradation:** if any requirement is missing (non-Linux, no toolchain,
|
||
addon not built), the addon loader (`src/mitm/tproxy/transparentSocket.ts::loadTransparentAddon`)
|
||
returns `null` rather than throwing. The capture-mode status then reports
|
||
`available: false`, the dashboard toggle is **disabled** with the tooltip
|
||
"TPROXY decrypt requires Linux + root + the native addon", and the rest of
|
||
OmniRoute keeps working.
|
||
|
||
---
|
||
|
||
## §3 The native IP_TRANSPARENT addon
|
||
|
||
Node's `net` module cannot `setsockopt(IP_TRANSPARENT)` _before_ `bind()`, which
|
||
TPROXY requires (otherwise the kernel drops the redirected packets). The addon
|
||
(`src/mitm/tproxy/native/transparent.c`, built via `binding.gyp`) is a small N-API
|
||
module exposing three functions, consumed through `transparentSocket.ts`:
|
||
|
||
| Addon function | Socket work | Used for |
|
||
| ------------------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||
| `createTransparentListener(ip, port)` | `socket()` + **SO_REUSEADDR** + **IP_TRANSPARENT** + `bind()` + `listen()`, returns the raw fd | the transparent capture listener (Node adopts the fd via `server.listen({ fd })`) |
|
||
| `setSocketMark(fd, mark)` | `setsockopt` **SO_MARK** on an existing fd | anti-loop (mark the proxy's own sockets) |
|
||
| `connectMarked(ip, port, mark)` | `socket()` + **SO_MARK** **before** a non-blocking `connect()`, returns fd | the re-encrypted upstream forward (the SYN carries the mark) |
|
||
|
||
The original destination is read from `socket.localAddress`/`localPort` — TPROXY
|
||
preserves it, so there is no **SO_ORIGINAL_DST**/NAT lookup.
|
||
|
||
### Building the addon
|
||
|
||
```bash
|
||
npm run build:native:tproxy # cd src/mitm/tproxy/native && node-gyp rebuild
|
||
# -> native/build/Release/transparent.node
|
||
```
|
||
|
||
- During `npm run build`, `scripts/build/build-tproxy-native.mjs` runs `node-gyp
|
||
rebuild`. It is **Linux-only and non-fatal** — a missing toolchain just leaves
|
||
the capture mode unavailable.
|
||
- `assembleStandalone.mjs` copies `build/Release/transparent.node` into the
|
||
standalone bundle; `transparentSocket.ts` resolves it both module-relative and
|
||
cwd-relative (`<cwd>/src/mitm/tproxy/native/...`).
|
||
- `build/` and `prebuilds/` are git-ignored — the binary is **built, never
|
||
committed**.
|
||
|
||
The loader probes, in priority order:
|
||
`native/build/Release/transparent.node`, then `native/prebuilds/transparent.node`
|
||
(both module-relative and under `<cwd>/src/mitm/tproxy/`).
|
||
|
||
---
|
||
|
||
## §4 The per-SNI dynamic CA and trust-store installer
|
||
|
||
The static AgentBridge MITM cert works only because AgentBridge DNS-spoofs a
|
||
**fixed** host set. TPROXY intercepts **arbitrary** hosts, so the listener must
|
||
present a valid leaf for whatever SNI the client requests.
|
||
|
||
### Dynamic CA (`src/mitm/tproxy/dynamicCert.ts`)
|
||
|
||
`DynamicCertStore` runs a local CA (built on the `selfsigned` dependency) that:
|
||
|
||
- Generates a long-lived CA via `generateMitmCa()` (CN `"OmniRoute MITM CA"`,
|
||
10-year validity, `basicConstraints CA=true` + `keyUsage keyCertSign,cRLSign`,
|
||
2048-bit RSA / SHA-256).
|
||
- Issues a **leaf per SNI hostname on demand** via `issueLeafCert()` (1-year
|
||
validity, `subjectAltName` = the SNI host) and caches one `tls.SecureContext`
|
||
per hostname.
|
||
- Exposes `createSNICallback()` for the TLS-terminating server (see [§5](#5-how-decrypt-and-capture-work)).
|
||
- Can be constructed with an `existingCa` to keep the CA stable across restarts
|
||
(so the trust store does not need re-installing).
|
||
|
||
The CA private key **never leaves the machine**.
|
||
|
||
### Trust-store installer (`src/mitm/tproxy/caTrust.ts`)
|
||
|
||
The intercepted client must trust the dynamic CA, so starting the capture mode
|
||
installs the CA cert into the OS trust store under a **dedicated slot** —
|
||
`omniroute-tproxy-ca.crt` (constant `TPROXY_CA_CERT_NAME`) — kept separate from
|
||
the static MITM cert's slot (`omniroute-mitm.crt`) so the two never clobber each
|
||
other.
|
||
|
||
`installTproxyCa(caPem, sudoPassword?)` detects the distro's anchor directory
|
||
(in order: Debian-style first) and runs the matching refresh command:
|
||
|
||
| Anchor directory | Refresh command |
|
||
| ------------------------------------------- | ------------------------ |
|
||
| `/usr/local/share/ca-certificates` | `update-ca-certificates` |
|
||
| `/etc/ca-certificates/trust-source/anchors` | `update-ca-trust` |
|
||
| `/etc/pki/ca-trust/source/anchors` | `update-ca-trust` |
|
||
| `/etc/pki/trust/anchors` | `update-ca-certificates` |
|
||
|
||
Install stages the PEM to a temp file, then (privileged) `mkdir -p` the anchor
|
||
dir, `cp` the staged file into it, and runs the refresh command. `uninstallTproxyCa()`
|
||
removes the dedicated slot only (leaving the static MITM cert untouched) and
|
||
refreshes — a no-op on non-Linux.
|
||
|
||
All privileged commands run via `execFileWithPassword` (`src/mitm/systemCommands.ts`)
|
||
— `spawn` with **arg arrays, no shell, no string interpolation** (Hard Rule #13).
|
||
When the process is root (e.g. the VPS) the target runs directly and no password
|
||
is needed; on a non-root desktop the `sudoPassword` is passed via `sudo -S` on stdin.
|
||
|
||
> The desktop's `sudoPassword` is supplied in the POST body to authorize the
|
||
> trust-store install; it is ignored entirely when the process is root.
|
||
|
||
---
|
||
|
||
## §5 How decrypt and capture work
|
||
|
||
The pipeline (all under `src/mitm/tproxy/`):
|
||
|
||
```
|
||
local app ──TCP/443──▶ mangle OUTPUT marks the conn (fwmark)
|
||
ip rule → local route table → lo
|
||
mangle PREROUTING TPROXY → IP_TRANSPARENT listener (port 8443)
|
||
│ captureMode.ts: reads orig dest from socket.localAddress
|
||
▼
|
||
tlsCapture.ts:
|
||
1. TLS-terminate the CLIENT with a per-SNI leaf (dynamicCert)
|
||
2. internal http.Server parses the decrypted plaintext
|
||
3. capture → globalTrafficBuffer.push() with source: "tproxy"
|
||
(sanitizeHeaders + maskSecret applied)
|
||
4. forward RE-encrypted to the original destination
|
||
over a bypass-marked socket (connectMarked, anti-loop)
|
||
│
|
||
▼
|
||
original upstream (api.example.com)
|
||
```
|
||
|
||
- **TLS termination** (`createTlsCaptureServer`): wraps the raw intercepted
|
||
socket in a server-side `tls.TLSSocket` using the dynamic CA's SNI callback,
|
||
then hands the decrypted stream to an internal `http.Server` (the standard MITM
|
||
termination trick). Socket lifetimes are bounded by `MITM_IDLE_TIMEOUT_MS` so a
|
||
hung tunnel cannot exhaust file descriptors.
|
||
- **Capture** (`handleDecryptedRequest`): pushes an `InterceptedRequest` with
|
||
`source: "tproxy"`, status starting `"in-flight"`, headers run through
|
||
`sanitizeHeaders()` and bodies through `maskSecret()` before they enter the
|
||
buffer. The entry is then updated with the response, sizes, and latency.
|
||
- **Re-encrypted forward** (`createForward` / `realForward`): re-encrypts to the
|
||
original destination. `rejectUnauthorized` defaults to **`true`** (secure by
|
||
default) — the upstream cert is verified against the SNI/Host the client
|
||
requested, so the proxy rejects exactly what the original client would.
|
||
|
||
### Anti-loop (SO_MARK)
|
||
|
||
Because the rules mark new local outbound connections, the proxy's **own**
|
||
re-encrypted forward would normally be re-intercepted — an infinite loop. The
|
||
forward path defends against this with a bypass socket mark (**SO_MARK**):
|
||
|
||
- `realForward` opens its upstream socket via `connectMarked(ip, port, DEFAULT_BYPASS_MARK)`
|
||
— `DEFAULT_BYPASS_MARK = 0x539` — which sets the **SO_MARK** **before** `connect()`,
|
||
so the forward's SYN carries the bypass mark.
|
||
- The `mangle OUTPUT` rule excludes connections already carrying the bypass mark
|
||
(`-m mark ! --mark <bypassMark>`), so the proxy's forward is **not** re-marked
|
||
and does not re-enter TPROXY.
|
||
|
||
> Implementation note: the bypass-marked socket must be installed on the agent's
|
||
> `createConnection` (`https.request({ createConnection })` is silently ignored
|
||
> when an agent is present), or the forward would open an unmarked socket and the
|
||
> loop would return. This was the e2e-validated anti-loop fix.
|
||
|
||
---
|
||
|
||
## §6 Security
|
||
|
||
| Control | Detail |
|
||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| **Loopback-only API** | `/api/tools/agent-bridge/tproxy` is covered by the `/api/tools/agent-bridge/` prefix in `LOCAL_ONLY_API_PREFIXES` (`src/server/authz/routeGuard.ts`). Loopback enforcement runs **before** auth (Hard Rules #15 + #17) — a leaked JWT over a tunnel cannot start TPROXY capture, which applies `iptables` rules and installs a trust-store CA via child processes. |
|
||
| **Dedicated CA slot** | The dynamic CA installs to `omniroute-tproxy-ca.crt`, never clobbering the static MITM cert. |
|
||
| **CA key never leaves the host** | `DynamicCertStore` holds the CA key in memory; it is not exported. |
|
||
| **Secret masking** | `maskSecret()` on request/response bodies and `sanitizeHeaders()` on headers run **before** `globalTrafficBuffer.push()`. |
|
||
| **No shell interpolation** | All `iptables`/`ip`/trust-store commands run via `execFile`/`execFileWithPassword` with arg arrays (Hard Rule #13). |
|
||
| **Upstream cert verification** | The re-encrypted forward verifies the upstream cert by default (`rejectUnauthorized: true`). |
|
||
| **Error sanitization** | The route's error responses go through `sanitizeErrorMessage()` (Hard Rule #12). |
|
||
|
||
**The MITM CA is a powerful capability.** A CA trusted by the OS that can sign any
|
||
host means anything OmniRoute intercepts can be decrypted. It is gated behind the
|
||
explicit, local-only TPROXY capture mode, off by default, and the trust-store
|
||
entry is removed when you stop the mode.
|
||
|
||
---
|
||
|
||
## §7 Transactional firewall apply / revert
|
||
|
||
A crash must never leave a `mangle` rule or stale route behind. The command builder
|
||
(`src/mitm/tproxy/commands.ts`) and runner (`src/mitm/tproxy/setup.ts`) guarantee
|
||
**revert is the exact inverse of apply, in reverse order**.
|
||
|
||
`applyTproxy(cfg)` runs the apply commands in order; on **any** failure it runs a
|
||
best-effort full `revertTproxy(cfg)` and rethrows — so the firewall is either
|
||
fully applied or fully reverted, never half-applied. `revertTproxy(cfg)` runs the
|
||
inverse commands in reverse order and swallows failures (idempotent — safe to call
|
||
unconditionally, e.g. from the AgentBridge `repairMitm()` cleanup).
|
||
|
||
`validateTproxyConfig(cfg)` runs before any command: ports must be `1–65535`,
|
||
`mark`/`routeTable`/`bypassMark` must be positive integers, and `bypassMark` must
|
||
differ from `mark` (anti-loop).
|
||
|
||
### Apply commands (in order)
|
||
|
||
```bash
|
||
ip rule add fwmark <mark> lookup <routeTable>
|
||
ip route add local 0.0.0.0/0 dev lo table <routeTable>
|
||
iptables -t mangle -A OUTPUT -p tcp --dport <dport> -m mark ! --mark <bypassMark> -j MARK --set-mark <mark>
|
||
iptables -t mangle -A PREROUTING -p tcp --dport <dport> -m mark --mark <mark> -j TPROXY --on-port <onPort> --tproxy-mark <mark>
|
||
```
|
||
|
||
Revert deletes them in reverse: `PREROUTING -D`, `OUTPUT -D`, `ip route del`, `ip rule del`.
|
||
|
||
> The recipe is **OUTPUT-based** because the MITM use case is _local_ outbound
|
||
> traffic (apps on the same host), which TPROXY in `PREROUTING` alone does not
|
||
> see — `PREROUTING` only sees forwarded traffic. The `OUTPUT` chain marks new
|
||
> local connections, the `ip rule` reroutes them to local delivery (`lo`), and
|
||
> `PREROUTING` then assigns them to the transparent listener.
|
||
|
||
---
|
||
|
||
## §8 Configuration
|
||
|
||
The start request (`POST /api/tools/agent-bridge/tproxy`) accepts the following
|
||
fields, validated by `StartTproxyBodySchema` (`tproxy/route.ts`). All are optional
|
||
and fall back to their defaults:
|
||
|
||
| Field | Type | Default | Notes |
|
||
| ---------------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------- |
|
||
| **dport** | int (1–65535) | `443` | Destination TCP port to transparently intercept |
|
||
| **mark** | int (≥1) | `0x2333` | Firewall mark set on `OUTPUT`, matched by the `ip rule` + `PREROUTING` |
|
||
| **onPort** | int (1–65535) | `8443` | Port the transparent (**IP_TRANSPARENT**) listener binds |
|
||
| **routeTable** | int (≥1) | `233` | Policy-routing table id holding the `local 0.0.0.0/0` route |
|
||
| **bypassMark** | int (≥1, ≠ `mark`) | `0x539` | The bypass socket mark (**SO_MARK**) the proxy sets on its own upstream conns; excluded in `OUTPUT` (anti-loop) |
|
||
| **sudoPassword** | string | — | Non-root desktops only: authorizes the trust-store install; ignored when root |
|
||
|
||
There are **no environment variables** for TPROXY — all configuration is via the
|
||
POST body or the defaults above.
|
||
|
||
---
|
||
|
||
## §9 Enabling from the Traffic Inspector
|
||
|
||
1. Open the **Traffic Inspector** (`/dashboard/tools/traffic-inspector`).
|
||
2. In the capture-modes toolbar, find the **"TPROXY Decrypt"** ⚠ button
|
||
(`src/app/(dashboard)/dashboard/tools/traffic-inspector/components/CaptureModesToolbar.tsx`).
|
||
- If it is **disabled** with the tooltip "TPROXY decrypt requires Linux + root +
|
||
the native addon", the native addon is unavailable on this host (non-Linux,
|
||
no toolchain, or addon not built). See [§2](#2-requirements) and [§3](#3-the-native-ip_transparent-addon).
|
||
3. Click the button. It calls `POST /api/tools/agent-bridge/tproxy` via
|
||
`startTproxyCaptureMode()` (`src/lib/inspector/tproxyCaptureApi.ts`), which:
|
||
builds the dynamic CA, opens the transparent listener, applies the firewall
|
||
rules, and installs the CA in the OS trust store.
|
||
4. When running, the toggle turns amber and shows the live intercept count
|
||
(`· <interceptCount>`). Intercepted requests appear in the request list with
|
||
`source: "tproxy"`.
|
||
5. Click again to stop — `DELETE /api/tools/agent-bridge/tproxy` via
|
||
`stopTproxyCaptureMode()` closes the listener, uninstalls the CA, and reverts
|
||
the firewall rules.
|
||
|
||
The capture-mode status (running / available / intercept count / listener port) comes
|
||
from `GET /api/tools/agent-bridge/tproxy` (`getCaptureStatus()` in
|
||
`src/mitm/tproxy/captureManager.ts`). Only **one** TPROXY session runs at a time —
|
||
starting a second rejects with "TPROXY capture mode is already running".
|
||
|
||
---
|
||
|
||
## §10 Troubleshooting
|
||
|
||
### Toggle is disabled
|
||
|
||
The native addon is not loadable. Confirm: you are on Linux, you built the addon
|
||
(`npm run build:native:tproxy`), and the process can load `transparent.node`.
|
||
`isTransparentSocketAvailable()` gates the toggle; `GET /api/tools/agent-bridge/tproxy`
|
||
returns `available: false` when the addon is missing.
|
||
|
||
### Nothing is captured
|
||
|
||
- Confirm the intercepted process actually connects to the configured `dport`
|
||
(default `443`).
|
||
- Confirm the process trusts the dynamic CA. The CA is installed under
|
||
`omniroute-tproxy-ca.crt`; apps with their own trust store (Firefox/Chrome NSS)
|
||
may need the cert added there too.
|
||
- Run the AgentBridge **Diagnose** self-test (see
|
||
[`AGENTBRIDGE.md`](../frameworks/AGENTBRIDGE.md)) for cert-trusted / server
|
||
health checks.
|
||
|
||
### Stale firewall rules after a crash
|
||
|
||
`revertTproxy()` is the exact inverse of apply and is idempotent. Stopping the
|
||
mode reverts the rules; if OmniRoute was killed mid-session, use the AgentBridge
|
||
**Repair** action (`POST /api/tools/agent-bridge/repair`) to undo orphaned system
|
||
state (DNS spoof, root CA, system proxy). The TPROXY `mangle` rules and route also
|
||
flush automatically on reboot.
|
||
|
||
### Infinite loop / the proxy intercepts its own forward
|
||
|
||
This is the anti-loop case. Confirm `bypassMark` differs from `mark` (validation
|
||
enforces this) and that the forward uses `connectMarked` (it does in `realForward`).
|
||
See [§5 Anti-loop](#anti-loop-so_mark).
|
||
|
||
---
|
||
|
||
## §11 Source map
|
||
|
||
| File | Responsibility |
|
||
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `src/mitm/tproxy/commands.ts` | Pure `iptables`/`ip` apply + revert command builder; `validateTproxyConfig` |
|
||
| `src/mitm/tproxy/setup.ts` | Transactional `applyTproxy` / `revertTproxy` runner (rollback on failure) |
|
||
| `src/mitm/tproxy/transparentSocket.ts` | Native-addon loader (`loadTransparentAddon`), `createTransparentListenerFd`, `connectMarked`, `setSocketMark`, `isTransparentSocketAvailable` |
|
||
| `src/mitm/tproxy/native/transparent.c` | N-API addon: `createTransparentListener` (IP_TRANSPARENT), `setSocketMark`, `connectMarked` |
|
||
| `src/mitm/tproxy/native/binding.gyp` | node-gyp build manifest |
|
||
| `src/mitm/tproxy/dynamicCert.ts` | `DynamicCertStore` — per-SNI dynamic CA + leaf cache |
|
||
| `src/mitm/tproxy/caTrust.ts` | OS trust-store install/uninstall (`installTproxyCa` / `uninstallTproxyCa`, dedicated slot) |
|
||
| `src/mitm/tproxy/tlsCapture.ts` | TLS-terminating decrypt engine + re-encrypted anti-loop forward |
|
||
| `src/mitm/tproxy/captureMode.ts` | Transparent-listener orchestration; reads orig dest from `socket.localAddress` |
|
||
| `src/mitm/tproxy/captureManager.ts` | Singleton lifecycle: `startCaptureMode` / `stopCaptureMode` / `getCaptureStatus` |
|
||
| `src/app/api/tools/agent-bridge/tproxy/route.ts` | `GET` / `POST` / `DELETE` route (LOCAL_ONLY) |
|
||
| `src/lib/inspector/tproxyCaptureApi.ts` | Client fetch helpers (`fetchTproxyStatus` / `startTproxyCaptureMode` / `stopTproxyCaptureMode`) |
|