Skip to content

From iptables to nftables: Enterprise Firewall on OpenWrt

OpenWrt 22.03 switched its firewall backend from iptables (fw3) to nftables (fw4). For hobbyists running a single router, the change was mostly invisible — UCI still works, LuCI still generates rules. But for us, managing 900+ SD-WAN appliances with custom enterprise rulesets layered on top of fw4, the migration was a significant undertaking.

This post covers why we migrated, how iptables and nftables differ in practice, the fw4 zone-based model, a sample enterprise ruleset, performance differences, and the pitfalls we hit along the way.

The short answer: iptables is deprecated. The kernel iptables interface is a compatibility layer on top of nftables in modern kernels. New features land in nftables only. Upstream OpenWrt has moved on, and fighting the platform is a losing strategy.

The longer answer: nftables is genuinely better for enterprise use cases.

  • Atomic rule replacement. In iptables, updating a ruleset means flushing chains and re-adding rules one by one. During the flush, there is a window where traffic is not filtered. nftables replaces entire rulesets atomically.
  • Single framework. iptables, ip6tables, arptables, and ebtables are separate tools with separate syntax. nftables handles all of them in one tool with one syntax.
  • Sets and maps. nftables has native support for IP sets, verdict maps, and concatenated matches. In iptables, you need ipset as a separate module. In nftables, sets are first-class citizens.
  • Better performance with large rulesets. nftables uses a virtual machine internally, compiling rules into bytecode. For rulesets with hundreds of rules (common in enterprise), this is measurably faster than iptables’ linear chain traversal.

For engineers used to iptables, the nftables syntax takes some adjustment. Here are common enterprise rules in both formats.

iptables:

Terminal window
iptables -A INPUT -i eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i eth0 -j DROP

nftables:

chain input {
type filter hook input priority 0; policy drop;
iifname "eth0" ct state established,related accept
}

iptables:

Terminal window
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8443 -j DNAT --to-destination 10.0.1.50:443
iptables -A FORWARD -i eth0 -p tcp -d 10.0.1.50 --dport 443 -j ACCEPT

nftables:

chain prerouting {
type nat hook prerouting priority -100;
iifname "eth0" tcp dport 8443 dnat to 10.0.1.50:443
}
chain forward {
type filter hook forward priority 0; policy drop;
iifname "eth0" ip daddr 10.0.1.50 tcp dport 443 accept
}

iptables + ipset:

Terminal window
ipset create blocked_ips hash:ip
ipset add blocked_ips 203.0.113.5
ipset add blocked_ips 198.51.100.0/24
iptables -A FORWARD -m set --match-set blocked_ips src -j DROP

nftables:

set blocked_ips {
type ipv4_addr
flags interval
elements = { 203.0.113.5, 198.51.100.0/24 }
}
chain forward {
type filter hook forward priority 0; policy drop;
ip saddr @blocked_ips drop
}

The nftables version does not need a separate tool. Sets are defined inline, support intervals natively, and are updated atomically.

OpenWrt’s fw4 is a zone-based firewall that generates nftables rules from UCI configuration. Understanding fw4 is essential because your custom rules must coexist with its generated ruleset.

fw4 creates the following nftables tables:

  • inet fw4 — the main table containing all firewall chains.
  • Chains are named by convention: input_<zone>, output_<zone>, forward_<zone>_<zone>, etc.

A zone definition in UCI:

config zone
option name 'lan'
list network 'lan'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'REJECT'
config zone
option name 'wan'
list network 'wan' 'wan6'
option input 'DROP'
option output 'ACCEPT'
option forward 'DROP'
option masq '1'
option mtu_fix '1'

fw4 translates this into nftables chains with jump targets. The key insight is that fw4 generates a chain forward with jumps to zone-specific forwarding chains. You can insert custom rules either through UCI (which regenerates the nftables rules) or by adding a custom nftables include file.

fw4 supports including custom nftables rule files:

config include
option path '/etc/nftables.d/enterprise.nft'
option position 'chain-pre'
option chain 'forward'

This is where we inject enterprise-specific rules that go beyond what UCI zone config can express.

Here is a simplified version of the enterprise ruleset we deploy to branch appliances. It handles WAN ingress hardening, LAN segmentation, rate limiting, and geo-blocking.

/etc/nftables.d/enterprise.nft
# Threat intelligence blocklist (updated daily via cron)
set threat_ips {
type ipv4_addr
flags interval
# Populated by /etc/cron.d/update-threat-list
}
# Geo-block: countries we never expect traffic from
set geo_blocked {
type ipv4_addr
flags interval
# Populated by /etc/cron.d/update-geo-list
}
# Rate limit new connections from WAN
chain wan_ratelimit {
ct state new meter wan_connrate { ip saddr limit rate over 50/second } drop
}
chain enterprise_forward {
# Drop known threats
ip saddr @threat_ips counter drop
ip daddr @threat_ips counter drop
# Geo-blocking
ip saddr @geo_blocked counter drop
# LAN segmentation: IoT cannot reach corporate
iifname "br-iot" oifname "br-corp" counter reject
# Corporate to server VLAN: only specific ports
iifname "br-corp" oifname "br-servers" tcp dport { 22, 80, 443, 3306, 5432 } accept
iifname "br-corp" oifname "br-servers" counter reject
# Guest: internet only, no internal access
iifname "br-guest" oifname "br-*" counter reject
iifname "br-guest" oifname "eth0" accept
}
chain enterprise_input {
# Rate limit on WAN interface
iifname "eth0" jump wan_ratelimit
# Allow management access only from VPN
iifname "wg0" tcp dport { 22, 443 } accept
iifname "eth0" tcp dport { 22, 443 } counter drop
# SNMP from monitoring server only
ip saddr 10.255.0.10 udp dport 161 accept
}

We benchmarked iptables (fw3) vs nftables (fw4) on identical hardware (Hopbox branch appliance) with a production-representative ruleset of approximately 200 rules across all chains.

Metriciptables (fw3)nftables (fw4)Difference
Rule load time~800ms~50ms16x faster
Forwarding throughput (1K rules)~920 Mbps~960 Mbps~4% improvement
New connection rate~45K conn/s~52K conn/s~15% improvement
Atomic ruleset swapNot supported<5msN/A
Memory usage (1K rules)~12 MB~4 MB3x less

The biggest practical difference is atomic rule replacement. With iptables, a ruleset update on a busy appliance would occasionally drop packets during the flush-and-reload. With nftables, the new ruleset is compiled and swapped in a single kernel transaction. Zero packet loss during updates.

We did not migrate all 900+ devices at once. The rollout was phased over three months.

Converted the enterprise ruleset from iptables syntax to nftables. Ran the new rules in a lab environment with traffic replay from production captures. Validated every rule by checking counters against expected traffic patterns.

Deployed to 10 branch offices across different customer profiles (retail, office, warehouse). Monitored for two weeks, comparing firewall logs between iptables and nftables devices to ensure identical allow/deny decisions.

Pushed the firmware update (OpenWrt with fw4) to devices in batches of 50-100 per week. Each batch was monitored for 48 hours before proceeding. Rollback firmware was staged on every device in case of issues.

Removed all iptables compatibility packages from the firmware image. Reclaimed flash storage and reduced the attack surface. Updated all documentation and runbooks.

These are the issues we encountered during migration. Save yourself the debugging time.

OpenWrt can run iptables-nft, which accepts iptables syntax but translates it to nftables in the kernel. This is a compatibility shim, not a migration. The generated nftables rules are ugly, hard to debug, and do not use nftables features like sets and maps. Do a proper rewrite.

In iptables, chain ordering within a table is implicit. In nftables, chains have explicit priority values. If your custom chain has the wrong priority, it might run before or after fw4’s chains, producing unexpected behavior.

# fw4's forward chain uses priority 0
# Your pre-filter chain should use a lower (earlier) priority
chain enterprise_prefilter {
type filter hook forward priority -10;
# These rules run BEFORE fw4's forward chain
}

If you use multiple WAN interfaces (common in SD-WAN), conntrack zone assignment must be configured correctly in nftables. fw4 handles this for zones defined in UCI, but custom rules that manipulate conntrack may need explicit zone assignment.

iptables: -j LOG --log-prefix "DROP: " nftables: log prefix "DROP: " drop

The logging in nftables is an action, not a target. You can combine it with other actions on the same rule. But the log format is different — if your log parsing pipeline expects iptables-format log lines, update your parsers.

In iptables, every rule has a packet/byte counter. In nftables, counters are optional and must be explicitly added with the counter keyword. If you rely on per-rule counters for monitoring, add them explicitly or you will get no data.

The migration from iptables to nftables is not optional for OpenWrt-based products — iptables is deprecated and fw4 is the present and future. The good news is that nftables is genuinely better: atomic updates, native sets, cleaner syntax, and better performance with large rulesets. The migration requires rewriting custom rules (do not rely on the iptables-nft shim), phased rollout (do not push to 900 devices on day one), and careful attention to chain priorities and fw4 integration. The result is a firewall framework that can handle enterprise-grade rulesets without the iptables-era compromises.

v1.7.9