← Back

Building a Transparent Proxy Gateway with trans_proxy

Building a Transparent Proxy Gateway with trans_proxy cover image

Transparent Proxying

A conventional proxy has to be configured by hand on every device. A transparent proxy works one layer down, at the gateway: LAN devices only point their default gateway and DNS at the proxy machine, and all TCP traffic gets intercepted and forwarded to the upstream proxy automatically. The devices themselves need no proxy configuration.

That is especially useful for devices where setting a proxy is awkward: smart TVs, IoT gadgets, game consoles.

How trans_proxy Works

trans_proxy is a transparent proxy written in Rust, running on macOS and Linux on top of the tokio async runtime. The overall architecture looks like this:

trans_proxy architecture diagram

Original Destination

The first problem a transparent proxy has to solve: once NAT has redirected the traffic to trans_proxy's listening port, how do you know where the client actually wanted to go?

trans_proxy does this differently on the two platforms:

  • macOS (pf): queries pf's NAT state table through the DIOCNATLOOK ioctl to get the destination the connection had before it was redirected
  • Linux (nftables): reads the original destination with a SO_ORIGINAL_DST getsockopt on the accepted socket

Resolving the Hostname

Once you have the original IP and port, you still have to turn the IP back into a domain name, because an upstream HTTP CONNECT proxy wants a request in CONNECT hostname:port form. trans_proxy picks the hostname in this order:

  1. SNI extraction: for TLS connections (port 443), parse the SNI extension in the ClientHello to get the domain the client is asking for, without decrypting any TLS traffic
  2. Reverse DNS lookup: if the built-in DNS forwarder is enabled, trans_proxy records DNS answers and can look up which domain an IP belongs to
  3. IP fallback: if neither works, just use the IP

Built-in DNS Forwarder

trans_proxy ships a DNS forwarder that listens on port 53 of the gateway interface. It provides:

  • DNS-over-HTTPS (DoH): sends DNS queries to an upstream DNS server over HTTPS (Cloudflare by default), avoiding DNS poisoning
  • HTTP/2 connection pool: reuses HTTP/2 connections to cut DoH query latency
  • TTL cache: caches answers according to the TTL on the DNS record
  • Query coalescing: merges concurrent queries for the same domain so the request is not repeated

Beyond serving DNS, the answers it records are what the hostname resolution above uses to map an IP back to a domain.

Firewall Integration

trans_proxy manages firewall rules through scripts:

  • macOS: uses pf's anchor mechanism, hanging the NAT redirect rules off a separate anchor so the system's existing pf rules are untouched
  • Linux: creates a separate nftables table, again without disturbing the existing firewall config

Build and Install

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

# Build (needs Rust 1.70+)
cargo build --release

# The binary lands in target/release/trans_proxy

Usage

macOS

Say the gateway machine's network interface is en0 and the upstream HTTP CONNECT proxy runs locally on 127.0.0.1:1082:

Bash
# Start trans_proxy (with the DNS forwarder enabled)
sudo ./trans_proxy --upstream-proxy 127.0.0.1:1082 --dns

# Set up the pf NAT redirect rules
sudo scripts/pf_setup.sh en0 8443

Linux

Say the gateway interface is eth0 and the upstream proxy is on 127.0.0.1:7890:

Bash
# Start trans_proxy
sudo ./trans_proxy --upstream-proxy 127.0.0.1:7890 --dns --interface eth0

# Set up the nftables rules
sudo scripts/nftables_setup.sh eth0 8443

Main Options

OptionDefaultDescription
--listen-addr0.0.0.0:8443Address trans_proxy listens on
--upstream-proxy(required)Upstream HTTP CONNECT proxy address
--dnsoffEnable the built-in DNS forwarder
--interfaceen0 / eth0Gateway network interface
--dns-upstreamCloudflare DoHUpstream DNS server (UDP or DoH)
--log-levelinfoLog level (trace/debug/info/warn/error)
-d / --daemonoffRun as a daemon

Client Setup

Devices on the LAN need two things:

  1. Set the default gateway to the IP of the machine running trans_proxy
  2. Set the DNS server to the same IP (if you enabled --dns)

macOS, iOS, Windows, Linux, Android and the other major platforms all support this, and nothing has to be installed on the device.

Install as a Service

trans_proxy can install itself as a system service in one step:

Bash
sudo ./trans_proxy --upstream-proxy 127.0.0.1:1082 --dns --install
  • macOS: creates a LaunchDaemon that starts at boot
  • Linux: creates a systemd unit, and uses ExecStartPre / ExecStopPost to load and clear the nftables rules automatically

To uninstall:

Bash
sudo ./trans_proxy --uninstall

Troubleshooting

When something goes wrong, turn up the log level first and watch:

Bash
sudo ./trans_proxy --upstream-proxy 127.0.0.1:1082 --dns --log-level debug

Common problems:

  • /dev/pf permission errors (macOS): make sure you are running as root
  • NAT lookup fails: check that the firewall rules loaded correctly (pfctl -sa on macOS, nft list ruleset on Linux)
  • Connection timeouts: confirm the upstream proxy is healthy, and check whether IP forwarding is on for the gateway machine (Linux needs sysctl net.ipv4.ip_forward=1)
  • DNS resolution fails: confirm no other process is holding port 53 (lsof -i :53)

Pairing with Caddy

trans_proxy needs an upstream HTTP CONNECT proxy. If you don't have one, the earlier post shows how to build one with Caddy. The typical combination is:

TEXT
LAN device → trans_proxy (gateway) → Caddy HTTPS forward proxy (remote server) → target site

That way LAN devices need no configuration at all and everything is forwarded through an encrypted tunnel.

Wrap-Up

The core problem trans_proxy solves is letting every device on a LAN go through a proxy without configuring anything. What you get:

  1. Zero-config clients: devices only change gateway and DNS, with no software to install and no proxy to set
  2. Cross-platform: works as a gateway on both macOS (pf) and Linux (nftables)
  3. DNS poisoning protection: the built-in DoH forwarder fixes DNS poisoning at the source
  4. Non-invasive firewall changes: the anchor and separate-table approach leaves existing rules alone
  5. Async performance: built on tokio, each connection scheduled independently, which suits the concurrency a gateway sees