I have been writing iptables rules since roughly forever, and I am fluent in its particular dialect of madness: the four tables, the chains that look the same but aren't, the way you can never quite remember whether -A appends before or after the rule you actually wanted. It works. I can do it half asleep. That is exactly why I kept putting off nftables for years.
This week the gateway box got rebuilt onto Debian 12, and I decided that if I was going to touch the firewall anyway I would do it properly. So: nftables.
The thing nobody tells you up front is how much smaller the result is. My old setup was four separate iptables invocations, an ip6tables set that was almost but not quite a copy, and a restore file that I was frankly scared of. The nftables version is one file, one address family that handles v4 and v6 together, and rules that read like sentences.
Here is the shape of it, trimmed:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
tcp dport { 22, 80, 443 } accept
tcp dport 51820 accept comment "wireguard"
}
}
The inet family is the headline feature for me. One ruleset, both protocols, no more maintaining a parallel ip6tables universe that inevitably drifts out of sync with the v4 one and lets something through I didn't mean to. The named sets ({ 22, 80, 443 }) compile to a real hash lookup rather than a chain of sequential comparisons, so adding a port doesn't add a rule to walk past on every packet.
A few things that tripped me up, so you don't have to:
- The default policy only applies if no rule matches, same as before, but
policy droplives on the chain declaration now, not as a separate command. Forget it and you have an accept-all firewall that looks busy. ct statereplaces the old conntrack match. Same idea, friendlier spelling.nft list rulesetis youriptables -S. It prints the live config in the exact syntax you'd write, which means you can dump, edit, and reload without translation. That alone is worth the move.
The migration tooling (iptables-restore-translate) got me about 80% of the way and produced something hideous, which I then rewrote by hand because the whole point was to end up with something I'd actually want to read in two years. Took an afternoon. The ruleset is now in the same git repo as the rest of the box config, it survives a reboot via the systemd unit, and when I nft list ruleset I understand every line.
I should have done this years ago. The old reflex was that iptables was the thing I knew and nftables was the thing I'd have to learn, and that trade never felt worth it on a quiet Tuesday. It is. The mental model is simpler, the file is shorter, and v4/v6 parity stops being a thing I have to remember. iptables isn't going anywhere, the kernel still speaks it through a compatibility layer, but I'm done writing it.