TPROXY vs REDIRECT: Transparent Socket Interception on Linux

When routing traffic through a Linux gateway, firewalls often need to steer transit packets into a local proxy daemon. Whether you are running an inspection filter, an authentication captive portal, or an anti-censorship tunnel on an OpenWrt router, intercepting connections requires a mechanism to catch packets that were never addressed to the local machine. Netfilter provides two primary mechanisms for this task: REDIRECT and TPROXY. While REDIRECT is tempting because of its minimal initial setup, it introduces architectural flaws that break UDP traffic and burden the kernel connection tracker. Netfilter TPROXY, combined with Linux policy routing, provides a non-destructive alternative.

The architectural flaws of REDIRECT

The standard REDIRECT target in iptables operates inside the PREROUTING chain of the NAT table. It works by performing destination network address translation (DNAT). The kernel rewires the packet destination IP address to the primary IP of the incoming interface or the loopback address 127.0.0.1. Once altered, the packet matches local socket bindings, and standard routing tables deliver it to user space.

This header rewriting creates immediate complications for TCP. Because the original destination address is overwritten in the IP header, the receiving daemon cannot determine where the client originally wanted to connect. To recover the original target, the socket application must query the Netfilter connection tracking table using the SO_ORIGINAL_DST socket option:

struct sockaddr_in orig_dst;
socklen_t addr_len = sizeof(orig_dst);
if (getsockopt(client_fd, SOL_IP, SO_ORIGINAL_DST, &orig_dst, &addr_len) < 0) {
    perror("getsockopt SO_ORIGINAL_DST");
    close(client_fd);
}

This conntrack lookup introduces a kernel-space hash table traversal on every new TCP handshake. Under heavy connection rates, conntrack lock contention spikes. More critically, REDIRECT breaks down when handling UDP. Because UDP is stateless, multiple incoming datagrams from different endpoints destined for different remote addresses arrive on the same bound socket. The SO_ORIGINAL_DST option does not reliably map connectionless datagrams, causing dropped packets or misrouted replies unless complex ancillary message parsing is configured on raw sockets.

How TPROXY preserves packet headers

Netfilter TPROXY takes an entirely different approach. Instead of mutating the IP packet headers, TPROXY leaves the source IP, destination IP, source port, and destination port completely untouched. It intercepts the packet during the Netfilter mangle table PREROUTING hook.

Unlike NAT-based redirection, TPROXY delivers the packet to user space with its wire headers intact. The proxy daemon binds to 0.0.0.0, enables IP_TRANSPARENT, and sees the real remote destination without querying conntrack tables.

When an incoming packet hits the TPROXY rule, the kernel Netfilter module performs a direct socket lookup in the local TCP or UDP listener hash table. If it finds a matching socket bound with the IP_TRANSPARENT flag, it attaches that socket directly to the kernel network buffer (sk_buff). The packet now belongs to that socket, even though its destination IP belongs to a remote server on the Internet.

Configuring policy routing and Netfilter rules

Because the packet still carries an external destination IP address, standard Linux route evaluation poses a challenge. Under normal circumstances, the kernel routing subsystem checks whether the destination address matches a local interface. If it does not, and IP forwarding is enabled, Linux tries to send the packet out through the WAN gateway interface. If IP forwarding is disabled, the kernel drops the packet.

To prevent this, you must instruct the Linux routing engine to treat these transit packets as local input. This requires combining packet marks (fwmark) with policy routing:

# Create custom routing table 100 to handle marked transparent traffic
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100

The local 0.0.0.0/0 dev lo route instructs the kernel that any packet matching table 100 must be delivered locally to the loopback interface, regardless of its actual IP address. Next, configure nftables to mark the packets and invoke the TPROXY engine:

table ip tproxy_gateway {
    chain prerouting {
        type filter hook prerouting priority mangle; policy accept;

        # Bypass private subnets and local interfaces
        ip daddr { 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } return

        # Direct TCP and UDP traffic to local port 12345
        meta l4proto { tcp, udp } th dport { 80, 443 } tproxy to 127.0.0.1:12345 meta mark set 1 accept
    }
}

Implementing IP_TRANSPARENT in application code

On the application side, the listening socket must be explicitly marked as transparent before binding. This tells the kernel network stack that the process is authorized to claim packets addressed to foreign IP addresses:

int fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;

if (setsockopt(fd, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt)) < 0) {
    perror("setsockopt IP_TRANSPARENT");
    close(fd);
    return 1;
}

struct sockaddr_in bind_addr;
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(12345);

if (bind(fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
    perror("bind");
    close(fd);
    return 1;
}
listen(fd, 128);

When a client connection completes via accept(), retrieving the target server address is simple. You call getsockname() on the newly returned client file descriptor. The socket structure already holds the foreign destination IP and port that the client originally dialed. No conntrack queries or external state caches are needed.

Operational constraints and socket permissions

While TPROXY resolves the structural flaws of REDIRECT, it requires specific operational privileges. Binding with IP_TRANSPARENT requires the CAP_NET_ADMIN capability. In containerized environments, running with full root or raw network administration privileges expands the attack surface. You should drop all capabilities except CAP_NET_ADMIN, or isolate the proxy inside its own network namespace.

Another operational consideration is reply packet routing. When the proxy process sends data back to the local client, the source address of the outbound packet must match the remote server IP the client expected. Setting IP_TRANSPARENT on the outgoing or accepted socket permits non-local address binding, allowing the socket to spoof the remote server address back to the LAN client.

For production gateways handling mixed TCP and UDP workloads, the architectural cleanliness of TPROXY outweighs the extra policy routing step. By keeping packet headers intact, you avoid connection tracking race conditions and maintain full protocol visibility across your network stack.

Press Cmd K to search برای جستجوی سایت از Cmd+K استفاده کنید