How OmniRoute's MITM Proxy Uses TPROXY Decryption to Capture CLI Traffic Ignoring Proxy Environment Variables
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) (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) (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.markvalue (e.g.,0x2333) usingiptables -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) (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). 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 incaptureMode.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), 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) (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.Serverto 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). 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) 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:
- Packet generation: The CLI creates a TCP socket to
api.example.com:443, unaware of any proxy configuration. - 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. - Routing diversion: The
ip ruledirects marked packets to the custom routing table, which routes them to the loopback interface (lo). - TPROXY interception: The PREROUTING rule captures the diverted packet and redirects it to the transparent listener socket bound to
cfg.onPort(e.g.,8443). - Destination extraction: The listener reads the original destination IP and port from the TPROXY metadata via
socket.localAddress. - 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.
- In raw mode, the system calls
- Traffic logging: All intercepted exchanges are written to the global traffic buffer (
globalTrafficBufferin [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:
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 lines 35–48.
Full TLS Decryption Setup
To capture and inspect HTTP plaintext, enable decrypt mode with a dynamic certificate store:
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) 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:
await handle.stop();
The stop() method (lines 176–185 in captureMode.ts) closes the listener socket, uninstalls the CA certificate via uninstallCa, and executes revertTproxy (lines 60–68 in setup.ts) to remove iptables rules and routing table entries.
Key Source Files and Responsibilities
src/mitm/tproxy/commands.ts: Builds exactiptablesandipcommands for TPROXY rule application and removal.src/mitm/tproxy/setup.ts: Executes firewall commands safely viaexecFileand manages rule lifecycle throughapplyTproxy()andrevertTproxy().src/mitm/tproxy/captureMode.ts: Orchestrates the transparent listener, connection handling, and mode selection (raw vs. decrypt).src/mitm/tproxy/transparentSocket.ts: ProvidescreateTransparentListenerFd()forIP_TRANSPARENTsockets andconnectMarked()for SO_MARK bypass sockets.src/mitm/tproxy/tlsCapture.ts: Implements TLS termination, HTTP parsing, and marked forwarding to upstream destinations.src/mitm/tproxy/dynamicCert.ts: Issues per-SNI leaf certificates from a dynamically generated CA for seamless TLS interception.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
bypassMarkandSO_MARKmechanism 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 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, 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 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →