Server

nftables for iptables Users

By Prabath Thalangama· October 24, 2026· 4 min read
#linux#firewall#nftables

Introduction

nftables is the successor to the xtables family (iptables, ip6tables, arptables, ebtables). One tool, one syntax, atomic ruleset reloads, native sets/maps, and better performance. Modern distros use it as the default backend (firewalld, ufw, and even iptables via iptables-nft shims sit on top).

The model

  • Family — inet (IPv4+IPv6 together — use this), ip, ip6, arp, bridge, netdev.
  • Table — a container you name. No built-in tables; you create them.
  • Chain — either a base chain (attached to a Netfilter hook with a priority and a policy) or a regular chain (jumped to, like a function).
  • Rule — match + statement (accept, drop, jump, counter, log, ...).

Hooks: prerouting, input, forward, output, postrouting. Priority numbers order multiple base chains on the same hook (lower runs first; filter = 0, nat around -100/100, mangle = -150).

A complete server ruleset

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    set ssh_allow {
        type ipv4_addr
        flags interval
        elements = { 203.0.113.0/24, 198.51.100.10 }
    }

    chain input {
        type filter hook input priority filter; policy drop;

        ct state established,related accept
        ct state invalid drop
        iif "lo" accept

        # ICMP / ICMPv6 — needed, don't blanket-drop
        ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept
        ip6 nexthdr icmpv6 icmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert,
             nd-router-advert, packet-too-big, destination-unreachable, time-exceeded } accept

        tcp dport 22 ip saddr @ssh_allow ct state new limit rate 5/minute accept
        tcp dport { 80, 443 } accept

        # log the rest before the policy drop
        limit rate 5/minute log prefix "nft-drop-in " level info
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
        ct state established,related accept
        # iifname "internal" oifname "wan" accept   # if this box routes
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

# NAT example (a gateway)
table inet nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname "wan" masquerade
    }
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;
        iifname "wan" tcp dport 8080 dnat ip to 10.0.0.5:80
    }
}
nft -f /etc/nftables.conf        # atomic load (flush + apply as one transaction)
systemctl enable --now nftables

Concept mapping

iptables nftables
-A INPUT -j ACCEPT rule in chain input
-m state --state ESTABLISHED,RELATED ct state established,related
-p tcp --dport 22 tcp dport 22
-s 10.0.0.0/8 ip saddr 10.0.0.0/8
ipset native set { ... }
separate iptables + ip6tables one inet table
-j LOG --log-prefix log prefix "..."
iptables-save / -restore nft list ruleset / nft -f

Sets and maps

# a set you update at runtime (fail2ban-style)
set blacklist { type ipv4_addr; flags dynamic, timeout; }
# add: nft add element inet filter blacklist { 198.51.100.66 timeout 1h }

# verdict map: port -> action
map port_action { type inet_service : verdict; elements = { 22 : jump ssh_chain, 80 : accept } }
tcp dport vmap @port_action

Maps and vmap replace long chains of rules with a hash lookup — much faster at scale.

Verification and troubleshooting

nft list ruleset                          # everything
nft list table inet filter
nft -a list chain inet filter input       # -a shows handles (for delete)
nft monitor trace                          # live rule tracing (add `meta nftrace set 1` to a rule)
nft list counters
  • Rules loaded but not matching — wrong family (ip table won't see IPv6; use inet), wrong hook/priority (a nat prerouting at the wrong priority runs after filter), or an earlier accept/drop short-circuited. nft monitor trace shows the path.
  • nft -f fails halfway — it shouldn't; nft applies atomically. A syntax error aborts the whole file with a line number and the old ruleset stays.
  • iptables and nftables rules both present — iptables-legacy vs iptables-nft, plus firewalld/docker. iptables -V shows which backend. nft list ruleset shows everything regardless. Don't mix hand-written nft with firewalld-managed tables.
  • Locked out — policy drop with no established accept, or the SSH rule is after the log/drop. Order: established → lo → allow rules → log → (policy).
  • ct state not working — conntrack module; on a pure filtering box it's fine, but ct state needs nf_conntrack loaded (it is, if any rule uses it).
  • Ruleset not persisting — /etc/nftables.conf not enabled via the nftables.service, or the distro expects it elsewhere. systemctl status nftables.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.