Tech Expert & Vibe Coder

With 14+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Creating nftables firewall rules with connection tracking to prevent Tailscale subnet router IP leaks during VPN reconnection storms

Why I Worked on This

I run a Tailscale subnet router on a Proxmox LXC container that forwards traffic from my home LAN through a designated exit node. This setup lets devices without Tailscale installed use my router as their gateway and still benefit from the VPN connection.

Everything worked fine until I started testing nftables mode. Tailscale added support for nftables in version 1.48.0, and I wanted to move away from iptables since nftables is becoming the standard on modern Linux systems. I set TS_DEBUG_FIREWALL_MODE=nftables in my tailscaled config and restarted the service.

Immediately, my subnet routing stopped working. Devices on the LAN could reach the router, but nothing made it to the internet. Packets went out, but replies never came back. I spent hours checking routing tables, interface configs, and Tailscale logs before realizing the problem was in the firewall rules themselves.

My Real Setup

My subnet router runs in an LXC container on Proxmox 8.3.2. The container is Debian 12 based, running tailscaled 1.78.3. I have IP forwarding enabled and Tailscale configured with:

  • --advertise-routes=192.168.1.0/24
  • --snat-subnet-routes=false
  • --exit-node=<my-exit-node-id>

The LAN devices use this container's IP as their default gateway. In normal iptables mode, Tailscale automatically adds a MASQUERADE rule in the nat table that rewrites source addresses for outgoing traffic. This makes return packets route correctly back through the tunnel.

When I switched to nftables mode, that masquerade rule was missing. I confirmed this by running nft list ruleset and comparing it to what I saw in iptables mode using iptables-save.

What Worked (and Why)

The fix required manually adding the missing masquerade rule to nftables. I created a rule in the postrouting chain of the nat table that matches traffic leaving through the Tailscale interface and masquerades it.

Here's what I added:

table ip nat {
    chain ts-postrouting {
        type nat hook postrouting priority 100; policy accept;
        oifname "tailscale0" masquerade
    }
}

I applied this with nft -f /etc/nftables.conf after adding the rule to my nftables config file. Immediately, subnet routing started working again. Return traffic flowed correctly, and devices on my LAN could reach the internet through the exit node.

The reason this works is that connection tracking needs the source address rewritten so replies know how to route back through the tunnel. Without masquerading, the exit node sees packets with private LAN source IPs, which don't route back correctly over the internet.

Making It Persistent

I also needed to handle Tailscale reconnections. During a "reconnection storm" — when the daemon restarts, the network flaps, or the exit node changes — Tailscale can briefly lose state. If connection tracking entries expire during this window, established connections break.

To prevent this, I added connection state tracking to the forward chain:

table ip filter {
    chain ts-forward {
        type filter hook forward priority 0; policy accept;
        ct state established,related accept
        iifname "tailscale0" accept
        oifname "tailscale0" accept
    }
}

This allows packets from existing connections to continue flowing even if Tailscale temporarily loses its internal routing state. The kernel's connection tracking table survives the restart and keeps the flows alive.

What Didn't Work

My first attempt was to rely on Tailscale's automatic nftables setup. I assumed that since iptables mode worked, nftables mode would handle masquerading the same way. It didn't. The logs showed Tailscale creating nftables chains, but the masquerade rule was never added.

I also tried setting --snat-subnet-routes=true, thinking Tailscale would handle the NAT internally. This didn't help in nftables mode. The issue wasn't with Tailscale's internal SNAT logic — it was that the firewall rules weren't being configured correctly at the netfilter level.

Another mistake was testing without connection tracking rules first. I added the masquerade rule and thought I was done, but then noticed that during brief network interruptions, active SSH sessions and file transfers would drop. Adding the connection state rules fixed that, but I should have included them from the start.

Key Takeaways

  • Tailscale's nftables mode is still in development. The TS_DEBUG_FIREWALL_MODE variable is explicitly marked as subject to change, and this behavior confirms it's not production-ready yet.
  • If you're running subnet routers or exit nodes with nftables, you need to manually add masquerade rules. Don't assume Tailscale will handle it automatically like it does in iptables mode.
  • Connection tracking rules are essential for surviving reconnection events. Without them, established flows break when Tailscale restarts or changes routes.
  • Always compare the actual firewall rules between modes. I wasted time troubleshooting routing and interface configs when the problem was just a missing NAT rule.
  • Use journalctl -ru tailscaled to verify which firewall mode is active. The logs clearly state whether nftables or iptables is being used, and why.

I'm keeping this setup for now because I want to stay on nftables long-term, but I'm also watching for updates to Tailscale that might add the missing rules automatically. Until then, manual configuration is the only reliable option.