Configuring Transparent Proxy with TPROXY on Linux: Redirecting All TCP/UDP Without Per-Client Configuration

Network tutorial - IT technology blog
Network tutorial - IT technology blog

Quick Start: Running TPROXY in 5 Minutes

Before diving into theory, let’s get a transparent proxy up and running right away. Environment: Ubuntu 22.04, kernel 5.15+, Squid as the proxy daemon.

Step 1: Install Squid with TPROXY Support

sudo apt update && sudo apt install squid

# Check if Squid was built with TPROXY support
squid -v | grep -i netfilter

The output must contain --enable-linux-netfilter. The default Ubuntu package already includes this flag.

Step 2: Configure Squid to Listen on the TPROXY Port

Add to /etc/squid/squid.conf:

# Port for TPROXY (different from the standard proxy port 3128)
http_port 3129 tproxy

# Temporarily allow all traffic for testing
http_access allow all

Step 3: Configure Kernel Routing + iptables

Create the file /usr/local/bin/tproxy_setup.sh:

#!/bin/bash
# Create a dedicated routing table for traffic with fwmark=1
ip rule add fwmark 1 lookup 100 2>/dev/null || true
ip route add local default dev lo table 100 2>/dev/null || true

# Load kernel module
modprobe xt_TPROXY

# Dedicated chain for easier management
iptables -t mangle -N TPROXY_MARK 2>/dev/null || iptables -t mangle -F TPROXY_MARK

# Redirect TCP ports 80 and 443 from LAN to Squid
iptables -t mangle -A TPROXY_MARK -p tcp -m multiport --dports 80,443 \
  -j TPROXY --tproxy-mark 0x1/0x1 --on-port 3129

# Skip traffic from the proxy daemon (avoid redirect loops)
iptables -t mangle -A PREROUTING -m owner --uid-owner proxy -j RETURN

# Apply to traffic from the LAN (adjust subnet to match your environment)
iptables -t mangle -A PREROUTING -s 192.168.1.0/24 -j TPROXY_MARK
sudo chmod +x /usr/local/bin/tproxy_setup.sh
sudo bash /usr/local/bin/tproxy_setup.sh
sudo systemctl restart squid

Test immediately from a client in the LAN — without configuring a proxy on the browser — visit any HTTP page, then check:

sudo tail -f /var/log/squid/access.log

If you see the client’s request appear in the log, TPROXY is working correctly.

Understanding Why TPROXY Is Needed

The Problem with Traditional Proxies

Traditional proxies have an inherent weakness: every device must be configured individually. A company with 200 computers — you need to push settings via GPO or MDM. Smart TVs, game consoles, IoT devices — most have no proxy option at all. IP cameras, network printers — almost never support proxies.

The solution lies at the kernel level. Traffic is intercepted right at the gateway before it leaves the internal network — clients don’t need to know, and don’t need to be configured. Smart TVs still browse the web normally, but every request flows through your proxy.

How TPROXY Differs from Standard NAT Redirect

With a standard NAT redirect (REDIRECT target), the proxy receives the packet but only sees its own address — the original destination information is completely lost. It’s like receiving a letter without knowing who the sender intended to deliver it to.

TPROXY is different at a key point: the original destination address is preserved when delivering the packet to userspace. The proxy reads the real destination via the IP_TRANSPARENT socket option, then connects to the internet using the client’s source IP. The destination server sees the request coming from 192.168.1.100, unaware that a proxy exists.

Full Packet Processing Flow

  1. Client sends a TCP SYN to 93.184.216.34:80
  2. Packet enters the eth0 interface and passes through the PREROUTING chain of the mangle table
  3. The TPROXY rule matches, marks the packet with fwmark=1, and redirects it to port 3129
  4. The kernel looks up a local socket — finds Squid listening on port 3129
  5. The packet is delivered to Squid, but with the original destination address preserved
  6. Squid reads the destination address 93.184.216.34:80 via getsockname()
  7. Squid opens a connection to the internet, binding the source IP to 192.168.1.100 (the client’s IP)

Advanced Configuration

Transparent UDP and DNS Handling

UDP has no handshake, so the mechanism differs from TCP, but TPROXY can still handle it. A common use case is intercepting DNS to filter or log all queries:

# Requires the xt_socket module for UDP transparency
modprobe xt_socket

# Add UDP DNS rule
iptables -t mangle -A TPROXY_MARK -p udp --dport 53 \
  -j TPROXY --tproxy-mark 0x1/0x1 --on-port 5353

Writing a UDP proxy in Python requires setting special socket options:

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Allow binding to addresses not belonging to this machine
sock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1)
# Receive the original destination address of each packet
sock.setsockopt(socket.SOL_IP, socket.IP_RECVORIGDSTADDR, 1)
sock.bind(('0.0.0.0', 5353))

Switching to nftables (Recommended for Newer Kernels)

Starting with Debian 11 and Ubuntu 22.04+, nftables is the default. The syntax is much cleaner than iptables, and the kernel team is prioritizing nftables development over further extending iptables:

table ip tproxy_table {
    chain prerouting {
        type filter hook prerouting priority mangle; policy accept;
        
        # Skip traffic from the proxy daemon
        meta skuid proxy return
        
        # Redirect TCP 80/443 from LAN
        ip saddr 192.168.1.0/24 tcp dport { 80, 443 } \
            tproxy ip to 127.0.0.1:3129 meta mark set 0x1
    }

    chain divert {
        type filter hook prerouting priority mangle;
        
        # Established socket traffic no longer needs TPROXY
        meta l4proto tcp socket transparent 1 meta mark set 0x1 accept
    }
}
sudo nft -f /etc/nftables-tproxy.conf
# Verify
sudo nft list ruleset

SSL Bump for HTTPS Inspection

In enterprise environments, inspecting HTTPS traffic is often required. Squid handles this with SSL bump — but you need to deploy the CA certificate to all clients first, otherwise browsers will show certificate errors:

# Generate a CA certificate first
openssl req -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 \
  -keyout /etc/squid/ssl_cert/myCA.key \
  -out /etc/squid/ssl_cert/myCA.pem

# squid.conf
https_port 3130 tproxy ssl-bump \
  cert=/etc/squid/ssl_cert/myCA.pem \
  key=/etc/squid/ssl_cert/myCA.key

# Only peek at the SNI, no decryption (less intrusive)
ssl_bump peek step1
ssl_bump splice all

Troubleshooting and Operational Tips

Debugging When Traffic Is Not Being Redirected

Checklist I use when TPROXY isn’t working:

# 1. Check if packets are hitting the rule
iptables -t mangle -L TPROXY_MARK -v -n

# 2. Check routing table 100
ip route show table 100
ip rule show

# 3. Check if Squid is bound to the correct port
ss -tlnp | grep 3129

# 4. Check the capabilities of the Squid process
cat /proc/$(pgrep squid | head -1)/status | grep -i cap

The most common issue: Squid lacks CAP_NET_ADMIN. Fix it by adding the following to the systemd service:

# /etc/systemd/system/squid.service.d/tproxy.conf
[Service]
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

# Reload and restart
sudo systemctl daemon-reload && sudo systemctl restart squid

Calculating Subnets When Setting Up Multiple VLANs

When adding rules for multiple subnets at once, I often use toolcraft.app/en/tools/developer/ip-subnet-calculator — enter a CIDR and get the network range, broadcast address, and host count instantly. Much more convenient than calculating by hand when adding rules for each VLAN.

Performance Tuning for High Connection Counts

# Increase the connection tracking table size
sysctl -w net.netfilter.nf_conntrack_max=262144

# Increase socket buffer sizes
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216

# Disable conntrack for traffic already handled by TPROXY (reduces overhead)
iptables -t raw -A PREROUTING -p tcp -m multiport --dports 80,443 \
  -m mark --mark 0x1 -j NOTRACK

Auto-start After Reboot with systemd

# /etc/systemd/system/tproxy-setup.service
[Unit]
Description=TPROXY iptables setup
Before=squid.service
After=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/tproxy_setup.sh
ExecStop=/usr/local/bin/tproxy_cleanup.sh

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now tproxy-setup.service

Once set up, you’re done. Every device on the LAN — smart TVs, IP cameras, IoT devices — flows through the transparent proxy. No need to touch the configuration on individual machines.

Share: