# How OmniRoute's MITM Proxy Uses TPROXY Decryption to Capture CLI Traffic Ignoring Proxy Environment Variables

> Learn how OmniRoute's MITM proxy uses TPROXY decryption to capture CLI traffic, bypassing proxy environment variables entirely by intercepting connections at the kernel level.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-07-08

---

**OmniRoute intercepts local outbound TCP connections at the kernel level using Linux TPROXY, bypassing the need for applications to respect `http_proxy` or `HTTPS_PROXY` environment variables entirely.**

The OmniRoute repository implements a transparent man-in-the-middle (MITM) proxy that leverages kernel-level packet interception to capture and decrypt HTTPS traffic from command-line tools and applications that explicitly ignore or lack proxy configuration. By operating below the user-space socket layer through TPROXY rules and `IP_TRANSPARENT` sockets, this solution captures traffic before it reaches the network stack's routing decision, making it invisible to applications and immune to proxy environment variable settings.

## Why Kernel-Level Interception Bypasses Proxy Settings

Traditional HTTP proxies rely on applications to check `http_proxy`, `HTTPS_PROXY`, or `ALL_PROXY` environment variables and voluntarily route traffic through the proxy. When CLIs ignore these variables—or when applications use hardcoded endpoints—standard proxy chains fail. OmniRoute solves this by intercepting packets at the **netfilter layer** using TPROXY, rerouting them to a local listener before they ever leave the host. Because the interception happens inside the kernel's networking stack, the application believes it is communicating directly with the target server, while OmniRoute transparently terminates and forwards the connection.

## The Five-Component Architecture

OmniRoute's TPROXY implementation consists of tightly coupled components spanning firewall rules, raw socket handling, and TLS termination.

### TPROXY Firewall Recipe

The firewall configuration is constructed in [[`src/mitm/tproxy/commands.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/commands.ts)](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/commands.ts) (lines 5–21, 31–45) and applied via `applyTproxy()` in [[`src/mitm/tproxy/setup.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/setup.ts)](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/setup.ts) (lines 35–48). This recipe performs three critical operations:

- **OUTPUT-mangle marking**: Tags new local outbound TCP packets destined for specific ports with a `cfg.mark` value (e.g., `0x2333`) using `iptables -t mangle -A OUTPUT -p tcp --dport ${dport} -j MARK --set-mark ${mark}`.
- **IP rule routing**: Adds a policy rule (`ip rule add fwmark ${mark} lookup ${routeTable}`) that directs marked packets to a dedicated routing table.
- **Local loopback routing**: Configures the custom routing table to deliver packets locally (`ip route add local 0.0.0.0/0 dev lo table ${routeTable}`), preventing them from reaching the physical network interface.
- **PREROUTING TPROXY interception**: Captures marked packets in the mangle table and redirects them to the transparent listener port (`iptables -t mangle -A PREROUTING -p tcp -m mark --mark ${mark} -j TPROXY --on-port ${onPort} --on-ip 127.0.0.1`).

The **bypassMark** mechanism (`cfg.bypassMark`) ensures that sockets created by the proxy itself are excluded from the OUTPUT rule, preventing infinite recursion when the proxy forwards traffic to upstream servers.

### Transparent Listener with IP_TRANSPARENT

The transparent listener is implemented in [[`src/mitm/tproxy/captureMode.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/captureMode.ts)](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/captureMode.ts) (lines 8–16, 30–40) using `createTransparentListenerFd()` from [[`src/mitm/tproxy/transparentSocket.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/transparentSocket.ts)](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/transparentSocket.ts). This function creates a raw socket with the **`IP_TRANSPARENT`** flag enabled, allowing the socket to bind to any IP address and receive packets that were originally destined for different endpoints. For each intercepted connection, the listener extracts the **original destination** from `socket.localAddress` (preserved by TPROXY) and reports it via the `onIntercept` callback.

### Raw Tunnel vs. Decryption Mode

The system supports two operational modes for handling intercepted connections:

- **Raw tunnel mode**: Pipes data transparently between the client and original destination using `handleTproxyConnection` (lines 31–48 in [`captureMode.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/captureMode.ts)), creating a simple TCP forward without inspecting payload contents.
- **Decrypt mode**: Hands the raw socket to the TLS termination engine in [[`src/mitm/tproxy/tlsCapture.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/tlsCapture.ts)](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/tlsCapture.ts), enabling full HTTPS inspection and modification of HTTP request/response bodies.

### TLS Termination Engine

In [[`src/mitm/tproxy/tlsCapture.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/tlsCapture.ts)](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/tlsCapture.ts) (lines 41–68, 89–118), the `createTlsCaptureServer` function wraps the intercepted socket in a `tls.TLSSocket` using certificates from a **dynamic CA** (`DynamicCertStore`). The engine:

- Generates per-SNI leaf certificates on-the-fly to terminate the client-side TLS handshake.
- Feeds the decrypted stream into an internal `http.Server` to parse HTTP semantics.
- Captures request/response details via `handleDecryptedRequest` (lines 65–80) before forwarding to the real destination.

### Anti-Loop Forwarding with SO_MARK

To prevent the proxy's own upstream connections from being re-intercepted, the forwarding mechanism uses `connectMarked()` (implemented in [`transparentSocket.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/transparentSocket.ts)). This function creates outbound sockets with the **`SO_MARK`** socket option set to `cfg.bypassMark`, ensuring that packets generated by the proxy match the exclusion criteria in the OUTPUT iptables rule. The `createForward` function (lines 89–118 in [`tlsCapture.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tlsCapture.ts)) builds a `realForward` implementation that opens these marked sockets before re-encrypting traffic to the original destination.

## Step-by-Step Traffic Capture Flow

When a CLI tool executes a request (e.g., `curl https://api.example.com/data`), the interception occurs through the following sequence:

1. **Packet generation**: The CLI creates a TCP socket to `api.example.com:443`, unaware of any proxy configuration.
2. **OUTPUT rule matching**: The kernel's netfilter mangle chain marks the outbound packet with `cfg.mark` (e.g., `0x2333`) because it matches the destination port criteria.
3. **Routing diversion**: The `ip rule` directs marked packets to the custom routing table, which routes them to the loopback interface (`lo`).
4. **TPROXY interception**: The PREROUTING rule captures the diverted packet and redirects it to the transparent listener socket bound to `cfg.onPort` (e.g., `8443`).
5. **Destination extraction**: The listener reads the original destination IP and port from the TPROXY metadata via `socket.localAddress`.
6. **Processing decision**: 
   - In **raw mode**, the system calls `connectMarked()` to create a bypass-marked upstream socket, then pipes data between client and server.
   - In **decrypt mode**, the socket enters `tlsCaptureServer.terminate`, which performs TLS handshake with a dynamically generated certificate, parses the HTTP request, and forwards it via the marked socket.
7. **Traffic logging**: All intercepted exchanges are written to the global traffic buffer (`globalTrafficBuffer` in [[`src/mitm/inspector/buffer.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/inspector/buffer.ts)](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/inspector/buffer.ts)) with the source identifier `"tproxy"`.

## Implementation Examples

### Minimal Raw-Tunnel Capture

This configuration intercepts HTTPS traffic without decrypting the payload, useful for simple forwarding or bandwidth monitoring:

```typescript
import { startTproxyCapture } from "./src/mitm/tproxy/captureMode";
import type { TproxyConfig } from "./src/mitm/tproxy/commands";

const cfg: TproxyConfig = {
  dport: 443,          // Target HTTPS traffic
  mark: 0x1234,        // Packet mark for routing
  onPort: 8443,        // Transparent listener port
  routeTable: 123,     // Custom routing table ID
};

await startTproxyCapture(cfg, {
  onIntercept: (info) => console.log(`Intercepted ${info.destIp}:${info.destPort}`),
});

```

This invokes `applyTproxy` to install firewall rules and creates a transparent listener using `createTransparentListenerFd`, piping raw sockets between client and upstream via `client.pipe(upstream)` as implemented in [`captureMode.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/captureMode.ts) lines 35–48.

### Full TLS Decryption Setup

To capture and inspect HTTP plaintext, enable decrypt mode with a dynamic certificate store:

```typescript
import { startTproxyCapture } from "./src/mitm/tproxy/captureMode";
import { myDynamicCertStore } from "./src/mitm/tproxy/dynamicCert";

const decryptOpts = {
  certStore: myDynamicCertStore,
  installCa: async (pem: string) => {
    // Install CA into OS trust store (e.g., update-ca-trust)
    await execFile("update-ca-trust", ["extract"]);
  },
  uninstallCa: async () => {
    // Cleanup CA from trust store
    await execFile("update-ca-trust", ["extract"]);
  },
};

const cfg = { 
  dport: 443, 
  mark: 0x2333, 
  onPort: 8443, 
  routeTable: 233 
};

const handle = await startTproxyCapture(cfg, { 
  decrypt: decryptOpts 
});

```

This configuration uses `createTlsCaptureServer` (lines 41–45 in [`tlsCapture.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tlsCapture.ts)) to terminate TLS and `realForward` (lines 66–68) to proxy requests over SO_MARK-marked sockets, preventing interception loops.

### Stopping the Intercept

To remove firewall rules and restore normal networking:

```typescript
await handle.stop();

```

The `stop()` method (lines 176–185 in [`captureMode.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/captureMode.ts)) closes the listener socket, uninstalls the CA certificate via `uninstallCa`, and executes `revertTproxy` (lines 60–68 in [`setup.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/setup.ts)) to remove iptables rules and routing table entries.

## Key Source Files and Responsibilities

- **[`src/mitm/tproxy/commands.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/commands.ts)**: Builds exact `iptables` and `ip` commands for TPROXY rule application and removal.
- **[`src/mitm/tproxy/setup.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/setup.ts)**: Executes firewall commands safely via `execFile` and manages rule lifecycle through `applyTproxy()` and `revertTproxy()`.
- **[`src/mitm/tproxy/captureMode.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/captureMode.ts)**: Orchestrates the transparent listener, connection handling, and mode selection (raw vs. decrypt).
- **[`src/mitm/tproxy/transparentSocket.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/transparentSocket.ts)**: Provides `createTransparentListenerFd()` for `IP_TRANSPARENT` sockets and `connectMarked()` for SO_MARK bypass sockets.
- **[`src/mitm/tproxy/tlsCapture.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/tlsCapture.ts)**: Implements TLS termination, HTTP parsing, and marked forwarding to upstream destinations.
- **[`src/mitm/tproxy/dynamicCert.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/tproxy/dynamicCert.ts)**: Issues per-SNI leaf certificates from a dynamically generated CA for seamless TLS interception.
- **[`src/mitm/inspector/buffer.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/mitm/inspector/buffer.ts)**: Central storage for intercepted traffic metadata and payloads.

## Summary

- **Kernel-level interception**: OmniRoute uses Linux TPROXY to capture traffic before it leaves the host, making proxy environment variables irrelevant to the interception process.
- **IP_TRANSPARENT sockets**: The transparent listener binds to foreign addresses and receives packets destined for remote servers, enabling true transparency.
- **Anti-loop protection**: The `bypassMark` and `SO_MARK` mechanism ensures the proxy's own upstream connections are not recursively intercepted.
- **Dual operation modes**: Supports both raw TCP tunneling for performance and full TLS decryption for HTTP inspection.
- **Zero client configuration**: Works with any CLI tool regardless of proxy support, including applications that explicitly disable or ignore proxy settings.

## Frequently Asked Questions

### Does OmniRoute require root privileges to intercept traffic?

Yes. Configuring TPROXY rules via `iptables` and `ip rule`, creating `IP_TRANSPARENT` sockets, and modifying the system CA trust store all require elevated privileges. The `applyTproxy()` function in [`setup.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/setup.ts) executes these commands using `execFile`, which typically requires running as root or with `CAP_NET_ADMIN` capabilities.

### How does OmniRoute prevent infinite loops when forwarding to upstream servers?

The implementation uses the `bypassMark` (`cfg.bypassMark`) value to exclude the proxy's own sockets from the OUTPUT iptables rule. When `connectMarked()` creates an upstream socket in [`transparentSocket.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/transparentSocket.ts), it sets the `SO_MARK` socket option to this bypass value. Consequently, packets generated by the proxy do not match the TPROXY marking criteria, preventing them from being redirected back to the listener.

### Can this intercept traffic from Docker containers or only host processes?

The current implementation focuses on host-level interception by marking packets in the OUTPUT chain. To intercept container traffic, the iptables rules would need extension to handle the Docker bridge network interfaces (typically `docker0`) or custom network namespaces. The core TPROXY mechanism supports this, but the specific [`commands.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/commands.ts) recipe targets local outbound traffic from the host network namespace.

### What happens to the original TLS certificates when decrypt mode is enabled?

OmniRoute generates dynamic leaf certificates for each destination server using the `DynamicCertStore` in [`dynamicCert.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/dynamicCert.ts). These certificates are signed by a locally controlled CA that must be temporarily installed into the system trust store (via `installCa`). The client sees a valid certificate chain for the target domain, while OmniRoute holds the private key to decrypt the traffic. When interception stops, `uninstallCa` removes the temporary CA to restore normal certificate validation.