The most surprising thing about nftables TPROXY is that it allows you to intercept and redirect network traffic to a proxy without needing to perform Source NAT on the original client’s IP address.

Let’s see this in action. Imagine we have a firewall (192.168.1.1) acting as our gateway, and we want all HTTP traffic from our internal network (192.168.1.0/24) to go through a transparent proxy running on the same firewall at 127.0.0.1:8080.

First, we need to enable IP forwarding on the firewall. This is standard for any device acting as a gateway.

sysctl net.ipv4.ip_forward=1

Now, let’s set up nftables to intercept TCP traffic destined for port 80 and redirect it.

nft add table ip mangle
nft add chain ip mangle prerouting { type filter hook prerouting priority -150 \; }
nft add chain ip mangle output { type filter hook output priority -150 \; }

nft add rule ip mangle prerouting ip saddr 192.168.1.0/24 tcp dport 80 meta mark set 0x1 tcp dport 80 redirect to 127.0.0.1:8080
nft add rule ip mangle output ip daddr 192.168.1.0/24 tcp dport 80 meta mark set 0x1 tcp dport 80 redirect to 127.0.0.1:8080

In this nftables configuration:

  • We create an ip mangle table. This table is used for altering packet headers.
  • We add prerouting and output chains.
    • prerouting hooks into the prerouting chain, which is the earliest point in the packet path where we can modify packets destined for other machines. This is where we’ll catch traffic originating from our internal network.
    • output hooks into the output chain, which handles packets originating from the firewall itself. This is important if you want to transparently proxy traffic that the firewall initiates, or if your proxy is on a different machine and the firewall needs to route traffic to it.
  • The -150 priority is crucial. It ensures our rules run before the routing decision is made (normal filter hook priority is 0). This allows us to redirect packets that would otherwise be routed elsewhere.
  • ip saddr 192.168.1.0/24: This matches packets originating from our internal subnet.
  • tcp dport 80: This matches standard HTTP traffic.
  • meta mark set 0x1: We are setting a firewall mark. While not strictly necessary for TPROXY itself in this simple example, it’s a common practice to mark packets that have been processed by TPROXY. This allows subsequent rules to easily identify and bypass TPROXY’d traffic, preventing infinite loops if the proxy itself were to generate traffic that needs to be proxied.
  • redirect to 127.0.0.1:8080: This is the core of TPROXY. Instead of using DNAT (Destination Network Address Translation) to change the destination IP and port, redirect tells the kernel to deliver the packet to the specified local IP address and port without altering the original destination IP address. The proxy process listening on 127.0.0.1:8080 will see the packet as if it came from the original client IP (192.168.1.x) and was destined for the original destination IP.

The key benefit here is that the proxy receives the original source IP address of the client. This is vital for applications that rely on the client’s true IP, such as web servers that log client IPs, or services that perform IP-based access control. With traditional Source NAT, the proxy would only see the firewall’s IP address as the source, losing the original client information.

The proxy itself needs to be configured to bind to 127.0.0.1 and listen on port 8080. It also needs to be aware that it’s acting as a transparent proxy. For example, with Squid, you’d configure:

http_port 127.0.0.1:8080 transparent

The transparent directive tells Squid to expect traffic that has been redirected via TPROXY and to preserve the original client IP information.

This TPROXY mechanism works by leveraging the SO_ORIGINAL_DST socket option. When a packet is TPROXY’d, the kernel makes the original destination IP and port available to the listening socket via getsockopt(sockfd, SOL_IP, IP_ORIGINAL_DST, &addr, &len). The proxy can then use this information to correctly process the request and potentially forward it to the actual original destination.

The primary challenge with TPROXY isn’t the setup itself, but ensuring your proxy application correctly supports and utilizes the SO_ORIGINAL_DST socket option. Many modern proxies do, but older or simpler ones might not.

After fixing TPROXY, the next common problem is dealing with UDP traffic, which requires a similar setup but uses different nftables actions and proxy configurations.

Want structured learning?

Take the full Nftables course →