TL;DR — Quick Summary
nftables replaces iptables as the modern Linux firewall. Learn nft syntax, table families, sets, NAT, rate limiting, and migration from iptables.
nftables is the official successor to iptables, ip6tables, arptables, and ebtables, merged into a single, unified framework in the Linux kernel since 4.x. If you are still writing firewall rules with iptables, this guide shows you why nftables is better, how to migrate your existing rules, and how to build a complete firewall from scratch using nft.
Prerequisites
- Linux kernel 4.x or later (Ubuntu 20.04+, Debian 10+, RHEL/CentOS 8+, Fedora 32+).
- Root or sudo access.
- Basic understanding of TCP/IP networking (ports, protocols, CIDR notation).
- Existing iptables rules to migrate (optional but helpful for context).
Why nftables?
The iptables ecosystem was four separate tools (iptables, ip6tables, arptables, ebtables) — one per address family. nftables replaces all of them:
- Unified IPv4/IPv6 — The
inetfamily handles both in one ruleset. - Atomic rule replacement — The entire ruleset is applied in one kernel transaction. No more partial updates that leave your firewall in an inconsistent state.
- Better syntax — One rule can match multiple ports, protocols, or addresses without repeating the entire rule.
- Sets and maps — Store thousands of IPs or port ranges as named kernel-side sets for O(1) lookup instead of linear chain scanning.
- Better performance — Fewer kernel round-trips. Large rulesets are significantly faster.
- Single command —
nftreplacesiptables,ip6tables,arptables, andebtables.
Migration from iptables
The iptables-nft Compatibility Layer
Most distributions ship iptables-nft, a drop-in replacement that runs iptables commands but stores rules in the nftables kernel subsystem. No code changes needed:
# Check which backend your iptables uses
iptables --version
# iptables v1.8.7 (nf_tables) ← already using nftables backend
# iptables v1.8.7 (legacy) ← still using iptables backend
Translating Rules with iptables-translate
# Translate a single rule
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# Output: nft add rule ip filter INPUT tcp dport 22 counter accept
# Translate an entire saved ruleset
iptables-save | iptables-restore-translate -f /etc/nftables.conf
Review the output — some constructs translate with counter added by default, and table/chain names follow iptables conventions (capitalized). You may want to rename them.
nft Command Basics
# List the entire ruleset
nft list ruleset
# List a specific table
nft list table inet filter
# List a specific chain
nft list chain inet filter input
# Flush everything (careful — removes all rules)
nft flush ruleset
# Load rules from a file
nft -f /etc/nftables.conf
# Add a table
nft add table inet filter
# Delete a table (and all its chains and rules)
nft delete table inet filter
Tables and Chains
Table Families
| Family | Handles |
|---|---|
ip | IPv4 packets only |
ip6 | IPv6 packets only |
inet | Both IPv4 and IPv6 (recommended for most rules) |
arp | ARP packets |
bridge | Packets traversing a Linux bridge |
netdev | Packets from a specific network device (ingress hook) |
Chain Types and Hooks
Base chains connect to netfilter hooks. The priority number controls ordering when multiple chains attach to the same hook (lower numbers run first):
| Chain Type | Valid Hooks | Common Priority |
|---|---|---|
filter | prerouting, input, forward, output, postrouting | 0 |
nat | prerouting, input, output, postrouting | -100 |
route | output | -150 |
# Create a base input filter chain (drop policy)
nft add chain inet filter input \
'{ type filter hook input priority 0 ; policy drop ; }'
# Create a forward chain
nft add chain inet filter forward \
'{ type filter hook forward priority 0 ; policy drop ; }'
# Create a regular chain (no hook — called via jump)
nft add chain inet filter tcp_allowed
Rule Syntax
Rules consist of matches (conditions) and verdicts (actions).
Common Matches
# Match by source IP
nft add rule inet filter input ip saddr 192.168.1.0/24 accept
# Match by destination port
nft add rule inet filter input tcp dport 443 accept
# Match multiple ports in one rule
nft add rule inet filter input tcp dport { 80, 443, 8080 } accept
# Match connection tracking state
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input ct state invalid drop
# Match by interface
nft add rule inet filter input iifname "lo" accept
nft add rule inet filter input iifname "eth0" tcp dport 22 accept
# Match by protocol
nft add rule inet filter input ip protocol icmp accept
nft add rule inet filter input ip6 nexthdr icmpv6 accept
Common Actions (Verdicts)
accept # Allow the packet
drop # Silently discard
reject # Discard and send ICMP unreachable (or TCP RST)
log # Log to kernel log (continues evaluation)
counter # Count packets (continues evaluation)
jump chain_name # Jump to another chain
return # Return from current chain
Combining Counters and Logging
# Count and log dropped packets
nft add rule inet filter input \
counter log prefix "nft-drop: " level warn drop
# Log SSH attempts
nft add rule inet filter input \
tcp dport 22 log prefix "SSH: " level info accept
Sets and Maps
Sets are the most powerful feature that iptables lacks natively.
Anonymous Sets (Inline)
# Allow HTTP and HTTPS in one rule
nft add rule inet filter input tcp dport { 80, 443 } accept
# Block specific source IPs
nft add rule inet filter input \
ip saddr { 10.0.0.5, 10.0.0.6, 10.0.0.7 } drop
Named Sets
# Create a named set of IPv4 addresses
nft add set inet filter blocked_ips { type ipv4_addr ; }
# Add elements
nft add element inet filter blocked_ips { 1.2.3.4, 5.6.7.8 }
# Use the set in a rule
nft add rule inet filter input ip saddr @blocked_ips drop
# Create a set with intervals (CIDR ranges)
nft add set inet filter trusted_nets \
{ type ipv4_addr ; flags interval ; }
nft add element inet filter trusted_nets \
{ 192.168.0.0/24, 10.0.0.0/8 }
Verdict Maps (vmaps)
Route traffic to different verdicts based on a key — faster than multiple rules:
# Map destination port to verdict
nft add map inet filter port_verdict \
{ type inet_service : verdict ; }
nft add element inet filter port_verdict \
{ 22 : accept, 80 : accept, 443 : accept, 25 : drop }
nft add rule inet filter input \
tcp dport vmap @port_verdict
NAT Configuration
NAT requires the ip (or ip6) family — not inet.
# Create NAT table and chains
nft add table ip nat
nft add chain ip nat prerouting \
'{ type nat hook prerouting priority -100 ; }'
nft add chain ip nat postrouting \
'{ type nat hook postrouting priority 100 ; }'
# Masquerade (SNAT for outgoing traffic — router/gateway)
nft add rule ip nat postrouting oifname "eth0" masquerade
# DNAT (port forwarding — redirect port 8080 to internal host)
nft add rule ip nat prerouting \
tcp dport 8080 dnat to 192.168.1.100:80
# Static SNAT (use a specific source IP)
nft add rule ip nat postrouting \
ip saddr 192.168.1.0/24 snat to 203.0.113.1
# Redirect (DNAT to local machine — transparent proxy)
nft add rule ip nat prerouting \
tcp dport 80 redirect to :3128
Rate Limiting
# Limit new SSH connections to 3 per minute per source IP
nft add rule inet filter input \
tcp dport 22 ct state new \
limit rate 3/minute burst 5 packets accept
# Limit ICMP ping rate
nft add rule inet filter input \
ip protocol icmp icmp type echo-request \
limit rate 10/second accept
# Drop packets exceeding the limit
nft add rule inet filter input \
tcp dport 22 ct state new \
limit rate over 10/minute drop
Logging
# Log with a custom prefix
nft add rule inet filter input \
log prefix "FIREWALL-DROP: " level warn
# Log levels: emerg, alert, crit, err, warn, notice, info, debug
nft add rule inet filter input \
tcp dport 22 log prefix "SSH-ACCESS: " level info accept
# Log to a specific log group (for ulogd2)
nft add rule inet filter input \
log group 0 prefix "nft: "
View logs: journalctl -k | grep FIREWALL-DROP or dmesg | grep FIREWALL.
Persistence: Saving and Restoring Rules
Save the Current Ruleset
nft list ruleset > /etc/nftables.conf
Load Rules from a File
nft -f /etc/nftables.conf
Enable the systemd Service
The nftables.service reads /etc/nftables.conf on start and flushes rules on stop:
systemctl enable --now nftables
systemctl status nftables
# Reload after editing /etc/nftables.conf
systemctl reload nftables
Example /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set trusted_ssh {
type ipv4_addr
flags interval
elements = { 192.168.1.0/24, 10.0.0.0/8 }
}
chain input {
type filter hook input priority 0 ; policy drop ;
iifname "lo" accept
ct state established,related accept
ct state invalid drop
ip saddr @trusted_ssh tcp dport 22 accept
tcp dport { 80, 443 } accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
limit rate 5/second log prefix "INPUT-DROP: " level warn
}
chain forward {
type filter hook forward priority 0 ; policy drop ;
}
chain output {
type filter hook output priority 0 ; policy accept ;
}
}
Practical Examples
Web Server (Nginx/Apache)
nft add table inet filter
nft add chain inet filter input \
'{ type filter hook input priority 0 ; policy drop ; }'
nft add rule inet filter input iifname "lo" accept
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input ct state invalid drop
nft add rule inet filter input tcp dport { 22, 80, 443 } accept
nft add rule inet filter input ip protocol icmp accept
nft add rule inet filter input ip6 nexthdr icmpv6 accept
Docker Host
Docker manages its own nftables/iptables chains in the nat and filter tables. To avoid conflicts, add custom rules to a separate table with higher priority, or use Docker’s DOCKER-USER chain:
# This chain is called after Docker's FORWARD rules
nft add chain inet filter DOCKER-USER \
'{ type filter hook forward priority -1 ; }'
nft add rule inet filter DOCKER-USER \
iifname "eth0" ip saddr != 10.0.0.0/8 drop
Gateway / Router
# Enable IP forwarding
sysctl -w net.ipv4.ip_forward=1
# NAT for outgoing traffic
nft add table ip nat
nft add chain ip nat postrouting \
'{ type nat hook postrouting priority 100 ; }'
nft add rule ip nat postrouting oifname "eth0" masquerade
# Forward chain — allow LAN to WAN, block WAN to LAN
nft add chain inet filter forward \
'{ type filter hook forward priority 0 ; policy drop ; }'
nft add rule inet filter forward \
iifname "eth1" oifname "eth0" ct state new,established,related accept
nft add rule inet filter forward \
iifname "eth0" oifname "eth1" ct state established,related accept
Comparison: nftables vs iptables vs firewalld vs ufw
| Feature | nftables | iptables | firewalld | ufw |
|---|---|---|---|---|
| IPv4 + IPv6 unified | Yes (inet) | No (separate tools) | Yes (via nftables) | Partial |
| Atomic rule updates | Yes | No | Yes | No |
| Sets / maps | Native | ipset (external) | Via rich rules | No |
| Syntax | Modern, concise | Verbose | XML / CLI | Simple CLI |
| NAT | Built-in | iptables-nat table | Via masquerade zones | Limited |
| Performance (large rulesets) | Excellent | Poor (linear scan) | Good | Fair |
| Direct kernel integration | Yes | Legacy (deprecated) | Frontend | Frontend |
| Learning curve | Moderate | Low (familiar) | Low | Very low |
Debugging with nft monitor and Counters
Watch Live Rule Matches
# Monitor all netfilter events in real time
nft monitor
# Watch only rule additions and deletions
nft monitor rules
Read Counter Values
# List all counters in a table
nft list table inet filter
# Reset counters without deleting rules
nft reset counters table inet filter
Test a Rule Without Applying It (Dry Run)
# Check syntax of a file without loading
nft -c -f /etc/nftables.conf
Trace Packet Path
# Add a trace rule (very verbose — use briefly)
nft add rule inet filter input meta nftrace set 1
nft monitor trace
Summary
- nftables unifies iptables, ip6tables, arptables, and ebtables into a single
nftcommand. - Use the
inetfamily to handle IPv4 and IPv6 in one ruleset. - Named sets and verdict maps replace ipset and complex multi-rule patterns for high-performance matching.
- Atomic rule replacement via
nft -feliminates the partial-update window that iptables suffers from. - Persist rules in
/etc/nftables.confand enablenftables.servicewith systemd. - Use
nft monitor traceand inline counters for debugging without restarting.