Shouldn't firewall rules be applied sequentially?

I have configured a firewall rule that accepts traffic from (zone lan && ip a.b.c.d) to (zone wan && port 53). This is followed by a rule that rejects any traffic to (zone wan && port 53).

When I look at the generated rules (in status), I see that

image

comes before

image

As can be expected from the generated rules, traffic doesn’t go through as I expected it when configuring the firewall rules.

Should I report this as a bug or did I miss something?

Maybe show the rule?
No, the status display does not show order of hooks processing packet.

You can also cross reference the upstream documentation at https://wiki.nftables.org/wiki-nftables/index.php/Main_Page

Did you create these rules via nftables (low level) or via the UCI (higher level) methods (UCI syntax covers the UCI CLI commands / direct /etc/config/firewall config file edits and also LuCI)?

Let's see the config:

Please connect to your OpenWrt device using ssh and copy the output of the following commands and post it here using the "Preformatted text </> " button (red circle; this works best in the 'Markdown' composer view in the blue oval):

Screenshot 2025-10-20 at 8.14.14 PM

Remember to redact passwords, VPN keys, MAC addresses and any public IP addresses you may have:

ubus call system board
cat /etc/config/firewall

I’ve taken an old router and reset it. ubus call system board:

{
	"kernel": "6.6.93",
	"hostname": "OpenWrt",
	"system": "ARMv7 Processor rev 5 (v7l)",
	"model": "AVM FRITZ!Box 4040",
	"board_name": "avm,fritzbox-4040",
	"rootfs_type": "squashfs",
	"release": {
		"distribution": "OpenWrt",
		"version": "24.10.2",
		"revision": "r28739-d9340319c6",
		"target": "ipq40xx/generic",
		"description": "OpenWrt 24.10.2 r28739-d9340319c6",
		"builddate": "1750711236"
	}
}

/etc/config/firewall (only the last two rules are added compared to reset state):

config defaults
	option syn_flood '1'
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'REJECT'

config zone
	option name 'lan'
	list network 'lan'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'

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

config forwarding
	option src 'lan'
	option dest 'wan'

config rule
	option name 'Allow-DHCP-Renew'
	option src 'wan'
	option proto 'udp'
	option dest_port '68'
	option target 'ACCEPT'
	option family 'ipv4'

config rule
	option name 'Allow-Ping'
	option src 'wan'
	option proto 'icmp'
	option icmp_type 'echo-request'
	option family 'ipv4'
	option target 'ACCEPT'

config rule
	option name 'Allow-IGMP'
	option src 'wan'
	option proto 'igmp'
	option family 'ipv4'
	option target 'ACCEPT'

config rule
	option name 'Allow-DHCPv6'
	option src 'wan'
	option proto 'udp'
	option dest_port '546'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-MLD'
	option src 'wan'
	option proto 'icmp'
	option src_ip 'fe80::/10'
	list icmp_type '130/0'
	list icmp_type '131/0'
	list icmp_type '132/0'
	list icmp_type '143/0'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-ICMPv6-Input'
	option src 'wan'
	option proto 'icmp'
	list icmp_type 'echo-request'
	list icmp_type 'echo-reply'
	list icmp_type 'destination-unreachable'
	list icmp_type 'packet-too-big'
	list icmp_type 'time-exceeded'
	list icmp_type 'bad-header'
	list icmp_type 'unknown-header-type'
	list icmp_type 'router-solicitation'
	list icmp_type 'neighbour-solicitation'
	list icmp_type 'router-advertisement'
	list icmp_type 'neighbour-advertisement'
	option limit '1000/sec'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-ICMPv6-Forward'
	option src 'wan'
	option dest '*'
	option proto 'icmp'
	list icmp_type 'echo-request'
	list icmp_type 'echo-reply'
	list icmp_type 'destination-unreachable'
	list icmp_type 'packet-too-big'
	list icmp_type 'time-exceeded'
	list icmp_type 'bad-header'
	list icmp_type 'unknown-header-type'
	option limit '1000/sec'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-IPSec-ESP'
	option src 'wan'
	option dest 'lan'
	option proto 'esp'
	option target 'ACCEPT'

config rule
	option name 'Allow-ISAKMP'
	option src 'wan'
	option dest 'lan'
	option dest_port '500'
	option proto 'udp'
	option target 'ACCEPT'

config rule
	option src 'lan'
	option dest 'wan'
	option name 'Allow DNS from LAN'
	option dest_port '53'
	option target 'ACCEPT'
	option family 'ipv4'

config rule
	option src '*'
	option dest 'wan'
	option name 'Deny direct DNS as default'
	option dest_port '53'
	option target 'DROP'

And finally nft -s list ruleset:

table inet fw4 {
	chain input {
		type filter hook input priority filter; policy drop;
		iif "lo" accept comment "!fw4: Accept traffic from loopback"
		ct state vmap { established : accept, related : accept } comment "!fw4: Handle inbound flows"
		tcp flags & (fin | syn | rst | ack) == syn jump syn_flood comment "!fw4: Rate limit TCP syn packets"
		iifname "br-lan" jump input_lan comment "!fw4: Handle lan IPv4/IPv6 input traffic"
		iifname "wan" jump input_wan comment "!fw4: Handle wan IPv4/IPv6 input traffic"
		jump handle_reject
	}

	chain forward {
		type filter hook forward priority filter; policy drop;
		ct state vmap { established : accept, related : accept } comment "!fw4: Handle forwarded flows"
		tcp dport 53 counter jump drop_to_wan comment "!fw4: Deny direct DNS as default"
		udp dport 53 counter jump drop_to_wan comment "!fw4: Deny direct DNS as default"
		iifname "br-lan" jump forward_lan comment "!fw4: Handle lan IPv4/IPv6 forward traffic"
		iifname "wan" jump forward_wan comment "!fw4: Handle wan IPv4/IPv6 forward traffic"
		jump handle_reject
	}

	chain output {
		type filter hook output priority filter; policy accept;
		oif "lo" accept comment "!fw4: Accept traffic towards loopback"
		ct state vmap { established : accept, related : accept } comment "!fw4: Handle outbound flows"
		oifname "br-lan" jump output_lan comment "!fw4: Handle lan IPv4/IPv6 output traffic"
		oifname "wan" jump output_wan comment "!fw4: Handle wan IPv4/IPv6 output traffic"
	}

	chain prerouting {
		type filter hook prerouting priority filter; policy accept;
		iifname "br-lan" jump helper_lan comment "!fw4: Handle lan IPv4/IPv6 helper assignment"
	}

	chain handle_reject {
		meta l4proto tcp reject with tcp reset comment "!fw4: Reject TCP traffic"
		reject comment "!fw4: Reject any other traffic"
	}

	chain syn_flood {
		limit rate 25/second burst 50 packets return comment "!fw4: Accept SYN packets below rate-limit"
		drop comment "!fw4: Drop excess packets"
	}

	chain input_lan {
		jump accept_from_lan
	}

	chain output_lan {
		jump accept_to_lan
	}

	chain forward_lan {
		meta nfproto ipv4 tcp dport 53 counter jump accept_to_wan comment "!fw4: Allow DNS from LAN"
		meta nfproto ipv4 udp dport 53 counter jump accept_to_wan comment "!fw4: Allow DNS from LAN"
		jump accept_to_wan comment "!fw4: Accept lan to wan forwarding"
		jump accept_to_lan
	}

	chain helper_lan {
	}

	chain accept_from_lan {
		iifname "br-lan" counter accept comment "!fw4: accept lan IPv4/IPv6 traffic"
	}

	chain accept_to_lan {
		oifname "br-lan" counter accept comment "!fw4: accept lan IPv4/IPv6 traffic"
	}

	chain input_wan {
		meta nfproto ipv4 udp dport 68 counter accept comment "!fw4: Allow-DHCP-Renew"
		icmp type echo-request counter accept comment "!fw4: Allow-Ping"
		meta nfproto ipv4 meta l4proto igmp counter accept comment "!fw4: Allow-IGMP"
		meta nfproto ipv6 udp dport 546 counter accept comment "!fw4: Allow-DHCPv6"
		ip6 saddr fe80::/10 icmpv6 type . icmpv6 code { mld-listener-query . 0, mld-listener-report . 0, mld-listener-done . 0, mld2-listener-report . 0 } counter accept comment "!fw4: Allow-MLD"
		icmpv6 type { destination-unreachable, time-exceeded, echo-request, echo-reply, nd-router-solicit, nd-router-advert } limit rate 1000/second burst 5 packets counter accept comment "!fw4: Allow-ICMPv6-Input"
		icmpv6 type . icmpv6 code { packet-too-big . 0, parameter-problem . 0, nd-neighbor-solicit . 0, nd-neighbor-advert . 0, parameter-problem . 1 } limit rate 1000/second burst 5 packets counter accept comment "!fw4: Allow-ICMPv6-Input"
		jump reject_from_wan
	}

	chain output_wan {
		jump accept_to_wan
	}

	chain forward_wan {
		icmpv6 type { destination-unreachable, time-exceeded, echo-request, echo-reply } limit rate 1000/second burst 5 packets counter accept comment "!fw4: Allow-ICMPv6-Forward"
		icmpv6 type . icmpv6 code { packet-too-big . 0, parameter-problem . 0, parameter-problem . 1 } limit rate 1000/second burst 5 packets counter accept comment "!fw4: Allow-ICMPv6-Forward"
		meta l4proto esp counter jump accept_to_lan comment "!fw4: Allow-IPSec-ESP"
		udp dport 500 counter jump accept_to_lan comment "!fw4: Allow-ISAKMP"
		jump reject_to_wan
	}

	chain accept_to_wan {
		meta nfproto ipv4 oifname "wan" ct state invalid counter drop comment "!fw4: Prevent NAT leakage"
		oifname "wan" counter accept comment "!fw4: accept wan IPv4/IPv6 traffic"
	}

	chain reject_from_wan {
		iifname "wan" counter jump handle_reject comment "!fw4: reject wan IPv4/IPv6 traffic"
	}

	chain reject_to_wan {
		oifname "wan" counter jump handle_reject comment "!fw4: reject wan IPv4/IPv6 traffic"
	}

	chain dstnat {
		type nat hook prerouting priority dstnat; policy accept;
	}

	chain srcnat {
		type nat hook postrouting priority srcnat; policy accept;
		oifname "wan" jump srcnat_wan comment "!fw4: Handle wan IPv4/IPv6 srcnat traffic"
	}

	chain srcnat_wan {
		meta nfproto ipv4 masquerade comment "!fw4: Masquerade IPv4 wan traffic"
	}

	chain raw_prerouting {
		type filter hook prerouting priority raw; policy accept;
	}

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

	chain mangle_prerouting {
		type filter hook prerouting priority mangle; policy accept;
	}

	chain mangle_postrouting {
		type filter hook postrouting priority mangle; policy accept;
		oifname "wan" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone wan IPv4/IPv6 egress MTU fixing"
	}

	chain mangle_input {
		type filter hook input priority mangle; policy accept;
	}

	chain mangle_output {
		type route hook output priority mangle; policy accept;
	}

	chain mangle_forward {
		type filter hook forward priority mangle; policy accept;
		iifname "wan" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone wan IPv4/IPv6 ingress MTU fixing"
	}

	chain drop_to_wan {
		oifname "wan" counter drop comment "!fw4: drop wan IPv4/IPv6 traffic"
	}
}

As you can see, chain forward drops the traffic to port 53 before it jumps to forward_lan. This does not match your expectations when you follow the sequence of the firewall rules, because there we have the acceptance …

config rule
	option src 'lan'
	option dest 'wan'
	option name 'Allow DNS from LAN'
	option dest_port '53'
	option target 'ACCEPT'
	option family 'ipv4'

before the drop:

config rule
	option src '*'
	option dest 'wan'
	option name 'Deny direct DNS as default'
	option dest_port '53'
	option target 'DROP'

That is because of the wildcard. Make the source of the drop lan and test again.

Try ths

config redirect
        option name 'Intercept-DNS'
        option src 'lan'
        option src_dport '53'
        option proto 'tcp udp'
        option family 'any'
        option target 'DNAT'
        option enabled '0'

What is the intent of disabling the rule? If it is disabled, it won't be created in the chain at all.

Slim hope OP finds the rule in list...

I'm not sure why you're suggesting disabling the rule when the OP really wants to know why the rules are applied out of the order they expect.

The reason the OP sees the (seemingly) out of order rules is because of the wildcard in the drop rule:

This causes the rule to be applied earlier in the chain than it would if the source zone is specified (i.e. lan). So the root cause is the wildcard in the source zone.

(disabling rules isn't going to help the OP here)

Of course, I can replace the src. And I can do other kinds of workarounds. But it makes things less obvious. What I want to express here is “disallow direct DNS access except for certain hosts from the LAN zone”. I’ve provided a minimal example in my previous posting, the real rule restricts the allowed access to two hosts in addition to requiring the LAN zone. The easiest workaround actually is to omit the zone from the accept rule. For the sake of completeness, here’s the accept rule as I’m using it currently (without the zone).

config rule                        
        option src '*'            
        option name 'Allow DNS for ACME verification and from host X'
        option dest_port '53'
        option target 'ACCEPT'
        option family 'ipv4' 
        option dest 'wan'     
        list src_ip '192.168.181.20'
        list src_ip '192.168.181.251'

Actually, I had the rule like this (without the zone) for years until I read recently that adding the zone is “best practice” (and this does make sense, so I tried it).

But all this is besides the point. The firewall configuration dialog suggests that rules are evaluated from top to bottom (sequentially). This is obviously not correct under all circumstances.

Either the behavior should be fixed or the circumstances that lead to a non-sequencial evaluation should be documented.

Different rule types land in different chains. Check nft list ruleset

It goes like

accept established
global rules
zone1 rules
zone2 rules
end verdict

I'm not sure why adding the source zone makes it it less obvious. In fact, I'd argue it makes it more clear.

But that said, @brada4 showed the order of operations for the firewall rulesets.... note that the global rules are above the specific zone rules. By making a wildcard src definition, the rule becomes a global which is applied earlier than the zone specific ones.

Reported as https://github.com/openwrt/openwrt/issues/21163.

I don't believe that this will be treated by the devs as a bug. As far as I'm aware, this is the intended behavior based on the order of precedence of the firewall rules in the chains as described by @brada4.

But given that you've already filed this as a bug, we'll see what happens.

If your problem is solved, please consider marking this topic as [Solved]. See How to mark a topic as [Solved] for a short how-to.
Thanks! :slight_smile:

1 Like

I explained with possible workaround. I dont think code build for dozen years will be turned upside down on a single whistle.

And here it comes.
Vlans. Each with a different zone.
Because why? Because we have a zone based firewall. You will safe a lot of time by using zones and just put host in zones but trying to replicate all rules for all hosts and all their exceptions will not work out.

Group your usecases and then find common rules and their exceptions and build your rules accordingly.

  1. Unlikely to be fixed. 2) +1. Unfortunately, docs are quite often the weak point of open source stuff. Good, old (antique ?) practices of sw-development are forgotten, nowadays: ‘Good software, which is not documented, does not exist’

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.