I've been meaning to do this for years and kept not doing it, because the iptables rules I had worked, and "works" is a strong argument against effort. But Buster ships nftables as the default backend now, and the writing is on the wall, so I bit the bullet on a low-stakes box.
The thing that surprised me is how much more readable the rules are. With iptables you have four tables, a pile of chains, and an ordering you have to hold in your head. nftables collapses that into one syntax where a ruleset actually reads like a description of intent:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
tcp dport { 22, 80, 443 } accept
ip protocol icmp accept
}
}
That tcp dport { 22, 80, 443 } set, in one line, would have been three separate iptables rules plus an ipset if I was feeling clever. And inet means the same ruleset covers IPv4 and IPv6, which alone justifies the move; I have lost real time to v6 rules silently not matching because I forgot ip6tables exists.
iptables-translate got me most of the way for the fiddly bits. I'd suggest translating, then rewriting by hand, because the machine translation is correct but ugly. It's worth doing properly while you're in there.