Why Your OpenWrt Transparent Proxy Leaks Packets

Last month, my home OpenWrt router started dropping SSH sessions every time a local device initiated heavy background traffic. Looking at the network logs, packets meant for the local TUN proxy interface skipped the custom routing table. They fell back to the default WAN gateway, creating an infinite loop that maxed out the router CPU.

I spent three hours isolating the problem. The issue was not the proxy daemon itself. The culprit was a silent precedence collision inside the Linux kernel routing policy database, triggered every time OpenWrt refreshed its network interfaces.

The Hidden Route Table Collision

When running transparent proxies on OpenWrt using virtual TUN interfaces, the system relies on netfilter marks to steer traffic. The proxy daemon creates a dedicated routing table in the kernel, typically numbered 100 or 202, and attaches policy rules using ip rule commands.

The breakdown occurs when local router processes generate outbound packets before the proxy initializes, or when OpenWrt’s netifd daemon handles a network hotplug event. Standard OpenWrt scripts insert routing rules with default priority values. If local daemons like dnsmasq or dropbear start before the proxy daemon finishes setting up its socket marks, the kernel matches the main routing table first.

When that happens, packets bypass the TUN device entirely. Unencrypted traffic leaks directly out of the physical Ethernet interface. Worse, if your firewall rules block unmarked WAN packets, the kernel drops the connection without notifying the sending application, leaving sockets hung indefinitely in a SYN_SENT state.

A proxy setup that relies solely on application-level socket marks will leak packets whenever the daemon restarts or drops its interface binding.

Fixing Policy Routing with IP Rules and NFTables

To prevent packet leaks, policy routing needs explicit numeric priorities and a hard fallback rule. You cannot rely on the proxy application to clean up its routing rules when it crashes or reloads.

First, assign fixed priorities to your custom policy routing rules. Make sure the lookup rule for your TUN table runs before the kernel checks the main table. Second, append an explicit unreachable rule directly after the proxy lookup. If the proxy daemon dies or its TUN interface goes down, traffic hits the unreachable rule and stops immediately instead of spilling into the WAN gateway.

Here is the shell configuration to check and enforce proper routing order on OpenWrt:

# Inspect existing policy rule priorities
ip rule show

# Insert explicit lookup rule for TUN table 100 before main table
ip rule add fwmark 0x1 lookup 100 pref 500
ip rule add unreachable pref 501

# Add nftables rule to mark outgoing TCP and UDP packets
nft add chain inet fw4 mangle_output { type filter hook output priority mangle \; }
nft add rule inet fw4 mangle_output meta mark 0x0 ip daddr != 192.168.1.0/24 counter meta mark set 0x1

Run ip route show table 100 after applying these commands. The output must list your virtual TUN device as the default gateway. If the default entry points to eth0 or br-lan, the rule priority order is incorrect.

To make these rules survive interface restarts, save them inside /etc/hotplug.d/iface/99-proxy-route rather than relying on manual start scripts. OpenWrt executes scripts in this directory whenever network interfaces toggle state, ensuring your fallback blackhole rules re-apply automatically.

Never test transparent proxy setups using standard ICMP ping. Ping commands use raw sockets that bypass several netfilter output hooks. Use traceroute -n -m 5 or monitor packets with tcpdump -i eth0 port 443 to confirm that outbound TCP sessions enter the TUN interface without hitting the physical upstream port.

Press Cmd K to search