trans_proxy

A transparent proxy for macOS and Linux that intercepts TCP traffic via pf/nftables and forwards it through an upstream HTTP CONNECT or SOCKS5 proxy.

Client Devices macOS pf rdr trans_proxy :8443 inside trans_proxy DIOCNATLOOK SNI Extract DNS Table CONNECT host:port Upstream Proxy (HTTP CONNECT / SOCKS5) Original Destination DNS Forwarder :53 gateway rdr IP → domain /dev/pf

Features

macOS pf Integration

Uses DIOCNATLOOK ioctl on /dev/pf to recover original destinations from pf's NAT state table — the same technique mitmproxy uses.

Linux nftables Integration

Uses SO_ORIGINAL_DST getsockopt to recover original destinations from nftables redirect rules.

SOCKS5 Upstream

Use a SOCKS5 proxy as the upstream via socks5://host:port, with optional username/password auth (RFC 1928/1929).

SNI Extraction

Peeks at the TLS ClientHello to extract hostnames, sending proper CONNECT host:port instead of raw IPs. No TLS termination or certificate generation needed.

DNS Forwarder

Listens directly on the gateway interface (port 53) for LAN client DNS queries. Builds an IP→domain lookup table. Supports DoH and UDP upstream.

Safe Firewall Rules

Uses anchor-based pf rules (macOS) or a dedicated nftables table (Linux) so your existing firewall configuration is never touched.

Daemon Mode

Run as a background process with PID file and log file support. Start with -d, stop with kill.

System Service

Install as a system service (launchd on macOS, systemd on Linux) for automatic startup on boot. On Linux, nftables NAT rules are auto-managed. Use --install and --uninstall.

Async I/O

Built on tokio with per-connection task spawning. Handles many concurrent connections efficiently with bidirectional relay.

E2E Tested

Full end-to-end test suite exercises the real nftables/pf + proxy pipeline on both Linux and macOS, covering SOCKS5, HTTP CONNECT, DNS forwarding, and port-selective redirect.

Requirements

Build

# Clone the repository
git clone https://github.com/madeye/trans_proxy.git
cd trans_proxy

# Build release binary
cargo build --release

# Verify
cargo test
./target/release/trans_proxy --help

Quick Start

This example assumes your upstream HTTP proxy runs on 127.0.0.1:1082 and your LAN interface is en0.

Start the transparent proxy

Run with DNS on the gateway interface (uses Cloudflare DoH by default):

# Foreground (auto-detects en0 IP, listens on port 53)
sudo ./target/release/trans_proxy \
  --upstream-proxy 127.0.0.1:1082 \
  --dns

# Or as a daemon
sudo ./target/release/trans_proxy \
  --upstream-proxy 127.0.0.1:1082 \
  --dns -d

Set up pf redirection

In another terminal, install the pf rules (HTTP/HTTPS only — DNS is handled directly):

sudo scripts/pf_setup.sh en0 8443

The script prints your gateway IP and setup summary.

Configure client devices

Set the Mac's IP as the default gateway (and DNS server) on each client device.

Tear down when done

sudo scripts/pf_teardown.sh
# If running as daemon
sudo kill $(cat /var/run/trans_proxy.pid)

CLI Reference

Flag Default Description
--listen-addr 0.0.0.0:8443 Address and port the proxy listens on
--upstream-proxy required Upstream proxy: host:port for HTTP CONNECT, socks5://host:port for SOCKS5
--log-level info Log verbosity: trace, debug, info, warn, error
--dns off Enable DNS forwarder on the gateway interface (port 53)
--interface en0 Network interface for DNS auto-detection
--dns-listen auto Override DNS listen address (e.g., 192.168.1.42:53)
--dns-upstream https://cloudflare-dns.com/dns-query Upstream DNS: host:port for UDP, or https:// URL for DoH
-d / --daemon off Run as a background daemon
--pid-file /var/run/trans_proxy.pid PID file path (used with --daemon)
--log-file /var/log/trans_proxy.log (daemon) Log file path (defaults to stderr in foreground)
--install off Install as a system service (launchd on macOS, systemd on Linux)
--uninstall off Uninstall the system service

pf Scripts

# Setup: pf_setup.sh <interface> [proxy_port]
# DNS no longer needs pf redirection — listens on port 53 directly
sudo scripts/pf_setup.sh en0 8443

# Teardown: flushes anchor rules, disables IP forwarding
sudo scripts/pf_teardown.sh

How It Works

Traffic Flow

  1. Client sends a packet to example.com:443 (resolved to 93.184.216.34)
  2. Packet arrives on the gateway's LAN interface
  3. NAT redirect rule rewrites the destination to 127.0.0.1:8443 (pf on macOS, nftables on Linux)
  4. trans_proxy accepts the connection
  5. Original destination recovered via DIOCNATLOOK (macOS) or SO_ORIGINAL_DST (Linux)
  6. SNI extraction peeks at the TLS ClientHello to read the hostname (example.com)
  7. Sends CONNECT example.com:443 to the upstream proxy
  8. Bidirectional relay between client and upstream proxy

Hostname Resolution Chain

1. SNI

Extracted from TLS ClientHello. Works for HTTPS (port 443). No TLS termination needed.

2. DNS Table

IP→domain mapping from DNS responses (via DoH or UDP). Works for HTTP and HTTPS. Requires --dns.

3. Raw IP

Falls back to the IP address if no hostname can be determined. Always available.

Original Destination Recovery

NAT redirect rules rewrite the destination address before the socket layer sees it. trans_proxy recovers the original destination using platform-specific mechanisms: DIOCNATLOOK on macOS (queries pf's NAT state table, same as mitmproxy) and SO_ORIGINAL_DST on Linux (recovers the pre-redirect destination from the kernel's conntrack).

Client Setup

On each device you want to route through the proxy, set the gateway machine's IP as the default gateway and DNS server.

macOS / iOS

Settings → Wi-Fi → (i) → Configure IP → Manual → Router & DNS: gateway IP

Windows

Settings → Network → Wi-Fi → Properties → Edit IP → Manual → Gateway & DNS: gateway IP

Linux

sudo ip route replace default via <ip> and update /etc/resolv.conf

Android

Wi-Fi → Long press → Modify → Advanced → Static IP → Gateway & DNS: gateway IP

Troubleshooting

"Failed to open /dev/pf"
Run with sudo. The proxy needs root to access /dev/pf.
"No ALTQ support in kernel"
This is a harmless warning from pfctl. macOS doesn't include ALTQ. pf redirection works fine without it.
"DIOCNATLOOK failed"
Ensure pf rules are loaded: sudo pfctl -a trans_proxy -s rules
Ensure pf is enabled: sudo pfctl -s info | head -1
Check that traffic is arriving on the expected interface.
Connections hang or timeout
Verify the upstream proxy is running and accepts CONNECT requests.
Use --log-level debug for detailed per-connection logging.
Ensure IP forwarding is enabled: sysctl net.inet.ip.forwarding (should be 1).
DNS not resolving on client devices
Ensure --dns is set and the DNS forwarder is running.
Check that trans_proxy logs show DNS forwarder listening on <ip>:53.
Test directly: dig @<gateway_ip> example.com