A transparent proxy for macOS and Linux that intercepts TCP traffic redirected by the OS firewall and forwards it through an upstream HTTP CONNECT or SOCKS5 proxy.
Designed to run on a machine acting as a side router (gateway) for other devices on the LAN.
[Client devices] --gateway--> [NAT redirect] --> [trans_proxy :8443]
|
v
[Upstream proxy (HTTP CONNECT / SOCKS5)]
|
v
[Original destination]
- macOS pf integration — Uses
DIOCNATLOOKioctl on/dev/pfto recover original destinations from pf's NAT state table - Linux nftables integration — Uses
SO_ORIGINAL_DSTgetsockopt to recover original destinations from nftables redirect - SOCKS5 upstream support — Use a SOCKS5 proxy as the upstream, with optional username/password authentication (RFC 1928/1929). Select via
socks5://host:portorsocks5://user:pass@host:port - SNI extraction — Peeks at TLS ClientHello to extract hostnames, sending proper
CONNECT host:portinstead of raw IPs - DNS forwarder — Listens directly on the gateway interface (port 53) for LAN client DNS queries, building an IP→domain lookup table. Supports DNS-over-HTTPS (DoH) with HTTP/2 connection pooling, TTL-aware caching, and query coalescing, as well as traditional UDP upstream.
- Anchor-based pf rules (macOS) / nftables table (Linux) — Won't clobber your existing firewall config
- Daemon mode — Run as a background process with PID file and log file support
- Service install — launchd on macOS, systemd on Linux. On Linux, nftables NAT rules are automatically managed via ExecStartPre/ExecStopPost
- Async I/O — Built on tokio with per-connection task spawning
- End-to-end tested — Full e2e test suite exercises the real nftables/pf + proxy pipeline on both Linux and macOS
- macOS: macOS 12+ (uses pf and
DIOCNATLOOKioctl) - Linux: Kernel 3.7+ with nftables
- Rust 1.70+ and Cargo (for building from source)
- Root privileges (for NAT lookups and port 53 binding)
- An upstream HTTP CONNECT or SOCKS5 proxy (e.g., Squid, Dante, ssh -D, or any CONNECT/SOCKS5-capable proxy)
# Clone the repository
git clone https://github.com/madeye/trans_proxy.git
cd trans_proxy
# Build release binary
cargo build --release
# Binary will be at ./target/release/trans_proxycargo test
./target/release/trans_proxy --helpThis example assumes your upstream proxy runs on 127.0.0.1:1082 and your LAN interface is en0.
# Step 1: Start the transparent proxy with DNS on the gateway interface
# HTTP CONNECT upstream:
sudo ./target/release/trans_proxy \
--upstream-proxy 127.0.0.1:1082 \
--dns
# Or with a SOCKS5 upstream:
# sudo ./target/release/trans_proxy \
# --upstream-proxy socks5://127.0.0.1:1080 \
# --dns
# Step 2: Set up pf redirection
sudo scripts/pf_setup.sh en0 8443
# Step 3: Configure client devices (see "Client Setup" below)
# Step 4: When done, tear down
sudo scripts/pf_teardown.sh
sudo kill $(cat /var/run/trans_proxy.pid)This example assumes your upstream proxy runs on 127.0.0.1:7890 and your LAN interface is eth0.
# Step 1: Start the transparent proxy with DNS
sudo ./trans_proxy \
--upstream-proxy 127.0.0.1:7890 \
--dns --interface eth0
# Step 2: Set up nftables redirection
sudo scripts/nftables_setup.sh eth0 8443
# Step 3: Configure client devices (see "Client Setup" below)
# Step 4: When done, tear down
sudo scripts/nftables_teardown.sh
sudo kill $(cat /var/run/trans_proxy.pid)The proxy requires root for NAT lookups (/dev/pf on macOS, SO_ORIGINAL_DST on Linux):
# Minimal — proxy only, no DNS
sudo ./target/release/trans_proxy \
--upstream-proxy <proxy_host>:<proxy_port>
# With DNS on the gateway interface (auto-detects en0 IP, listens on port 53)
sudo ./target/release/trans_proxy \
--upstream-proxy <proxy_host>:<proxy_port> \
--dns
# Specify a different interface
sudo ./target/release/trans_proxy \
--upstream-proxy <proxy_host>:<proxy_port> \
--dns --interface en1
# Override DNS listen address manually
sudo ./target/release/trans_proxy \
--upstream-proxy <proxy_host>:<proxy_port> \
--dns-listen 192.168.1.42:53
# Use a specific DoH provider
sudo ./target/release/trans_proxy \
--upstream-proxy <proxy_host>:<proxy_port> \
--dns --dns-upstream https://dns.google/dns-query
# Use traditional UDP DNS instead of DoH
sudo ./target/release/trans_proxy \
--upstream-proxy <proxy_host>:<proxy_port> \
--dns --dns-upstream 8.8.8.8:53
# Run as a background daemon
sudo ./target/release/trans_proxy \
--upstream-proxy 127.0.0.1:1082 \
--dns -d
# Daemon with custom PID and log file
sudo ./target/release/trans_proxy \
--upstream-proxy 127.0.0.1:1082 \
--dns -d --pid-file /tmp/trans_proxy.pid \
--log-file /tmp/trans_proxy.log
# Use a SOCKS5 upstream proxy
sudo ./target/release/trans_proxy \
--upstream-proxy socks5://127.0.0.1:1080 \
--dns
# SOCKS5 with username/password authentication
sudo ./target/release/trans_proxy \
--upstream-proxy socks5://user:pass@127.0.0.1:1080 \
--dns
# Redirect only specific ports (default: all TCP)
sudo ./target/release/trans_proxy \
--upstream-proxy 127.0.0.1:1082 \
--dns --ports 22,80,443| Flag | Default | Description |
|---|---|---|
--listen-addr |
0.0.0.0:8443 |
Address and port the proxy listens on |
--upstream-proxy |
(required) | Upstream proxy: host:port or http://host:port for HTTP CONNECT, socks5://host:port or socks5://user:pass@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 (macOS) / eth0 (Linux) |
Network interface for DNS auto-detection (used with --dns) |
--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) / stderr |
Log file path |
--local-traffic |
off | Also intercept traffic originating from the gateway itself (not just forwarded LAN traffic) |
--fwmark |
1 |
Firewall mark for loop prevention on Linux (used with --local-traffic) |
--ports |
(all TCP) | Comma-separated list of TCP ports to redirect (e.g., 22,80,443). When omitted, all TCP traffic is redirected |
--install |
off | Install as a system service (launchd on macOS, systemd on Linux) |
--uninstall |
off | Uninstall the system service |
The included scripts manage pf rules via an anchor (won't interfere with existing firewall rules).
sudo scripts/pf_setup.sh <interface> [proxy_port] [upstream_proxy] [ports]
sudo scripts/pf_setup.sh en0 8443 # all TCP
sudo scripts/pf_setup.sh en0 8443 "" 80,443 # only ports 80,443
sudo scripts/pf_setup.sh en0 8443 127.0.0.1:1082 # all TCP + local traffic
# Tear down
sudo scripts/pf_teardown.shThe included scripts create a dedicated nftables table for trans_proxy.
sudo scripts/nftables_setup.sh <interface> [proxy_port] [fwmark] [upstream_proxy] [ports]
sudo scripts/nftables_setup.sh eth0 8443 # all TCP
sudo scripts/nftables_setup.sh eth0 8443 "" "" 80,443 # only ports 80,443
sudo scripts/nftables_setup.sh eth0 8443 1 127.0.0.1:7890 # all TCP + local traffic
# Tear down
sudo scripts/nftables_teardown.shFor high-throughput proxy workloads, optimize kernel parameters and file descriptor limits:
sudo scripts/optimize_linux.shThis tunes sysctl settings (TCP buffers, backlog, connection recycling, TCP Fast Open) and raises file descriptor limits. Based on shadowsocks optimization guide.
Run trans_proxy as a background process:
# Start as daemon
sudo ./target/release/trans_proxy \
--upstream-proxy 127.0.0.1:1082 \
--dns -d
# Check status
cat /var/run/trans_proxy.pid
tail -f /var/log/trans_proxy.log
# Stop
sudo kill $(cat /var/run/trans_proxy.pid)In daemon mode:
- The process forks into the background and detaches from the terminal
- A PID file is written (default
/var/run/trans_proxy.pid) - Logs are written to a file (default
/var/log/trans_proxy.log) instead of stderr - The PID file is cleaned up on exit
Install trans_proxy as a system service for automatic startup on boot:
sudo ./target/release/trans_proxy \
--upstream-proxy 127.0.0.1:1082 \
--dns --installOn macOS, this installs a LaunchDaemon. On Linux, this installs a systemd service with automatic nftables setup/teardown — NAT redirect rules are created when the service starts and removed when it stops.
To uninstall:
sudo trans_proxy --uninstallBy default, trans_proxy only intercepts forwarded traffic from LAN clients passing through the gateway. To also intercept traffic originating from the gateway machine itself, use --local-traffic:
sudo ./target/release/trans_proxy \
--upstream-proxy 127.0.0.1:1082 \
--dns --local-traffic --installLoop prevention is automatic — no dedicated system user required:
- Linux: Sets
SO_MARK(fwmark) on outbound sockets; nftables OUTPUT chain skips marked packets - macOS: Sets
IP_BOUND_IFto bind outbound sockets tolo0when the upstream is on localhost, plus apass out quickpf rule to exclude the upstream proxy destination
On each device you want to route through the proxy:
- Set the default gateway to the Mac's IP address (shown by the setup script)
- Set the DNS server to the Mac's IP address (if using
--dns)
Settings → Wi-Fi → (i) → Configure IP → Manual → Router: <gateway_ip>, DNS: <gateway_ip>
Settings → Network → Wi-Fi → Properties → Edit IP → Manual → Gateway: <gateway_ip>, DNS: <gateway_ip>
sudo ip route replace default via <gateway_ip>
echo "nameserver <gateway_ip>" | sudo tee /etc/resolv.confSettings → Wi-Fi → Long press network → Modify → Advanced → IP settings: Static → Gateway: <gateway_ip>, DNS: <gateway_ip>
- Client device sends a packet to
example.com:443(resolved to e.g.,93.184.216.34) - Packet arrives on the gateway's LAN interface
- NAT redirect rule rewrites the destination to
127.0.0.1:8443(pf on macOS, nftables on Linux) - trans_proxy accepts the connection
- Original destination is recovered (
DIOCNATLOOKon macOS,SO_ORIGINAL_DSTon Linux) - trans_proxy peeks at the TLS ClientHello to extract SNI (
example.com) - Sends
CONNECT example.com:443to the upstream proxy (HTTP CONNECT or SOCKS5) - Bidirectional relay between client and upstream proxy
The proxy resolves hostnames for CONNECT requests using a fallback chain:
- SNI extraction — Parses the TLS ClientHello to read the Server Name Indication extension (port 443 only). No TLS termination or certificate generation required.
- DNS table lookup — If
--dnsis enabled, the built-in DNS forwarder records IP→domain mappings from A record responses. Works for both HTTP (port 80) and HTTPS (port 443). - Raw IP — Falls back to the IP address if no hostname can be determined.
NAT redirect rules rewrite the destination address before the socket layer sees it. trans_proxy recovers the original destination using platform-specific mechanisms:
- macOS:
DIOCNATLOOKioctl on/dev/pfqueries pf's NAT state table (same approach as mitmproxy) - Linux:
SO_ORIGINAL_DSTgetsockopt on the accepted socket fd recovers the pre-redirect destination
Run with sudo. The proxy needs root to access /dev/pf.
This is a harmless warning from pfctl. macOS doesn't include ALTQ — pf redirection works fine without it.
- 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 actually arriving on the expected interface
- Ensure nftables redirect rules are active:
sudo nft list table ip trans_proxy - Ensure IP forwarding is enabled:
sysctl net.ipv4.ip_forward(should be1)
- Verify the upstream proxy is running and accepts CONNECT requests
- Check with
--log-level debugfor detailed per-connection logging - Ensure IP forwarding is enabled
- Ensure
--dnsis set and the DNS forwarder is running - Check that trans_proxy logs show
DNS forwarder listening on <ip>:53 - Test:
dig @<gateway_ip> example.com