Firewall4 / NFtables Tips and Tricks

Hopefully this topic can help those getting their feet wet with NFtables, and maybe even help some of the seasoned NFtables veterans out there. Basically, this is for sharing and caring! If you have a neat NFtables tip or trick that you think might benefit others, share a snippet here for the good of the community.

In the spirit of sharing, I'll drop a little snippet here that others might find beneficial to include on their WAN ingress:

chain inbound_wan {
        # Drop all fragments.
        ip frag-off & 0x1fff != 0 counter drop

        # Drop XMAS packets.
        tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg counter drop

        # Drop NULL packets.
        tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop

        # Drop uncommon MSS values.
        tcp flags syn tcp option maxseg size 1-535 counter drop

        # Drop RFC2827 - IETF BCP38 / Bogons / Martian Packets
        ip saddr {,,,,,,,,,,,, } counter drop

Your turn! :smiley:


Mmmh, keep in mind when doing that, that MSS clamping is the easiest way to confirm sqm overhead settings by running speedtests at low packet sizes. So remember to disable the nft section above during the test.


Great to know! Thanks!

Here's another one to demonstrate a use of the nftables verdict map:

# Allow traffic only from established and related packets.
ct state vmap { established : accept, related : accept, invalid : drop }

This would be something you might include in your input and forward chains, for example.

1 Like

Some good security tips here:

Question, I want to match packets coming in on a particular VLAN with ether MAC sources in a set (of my kids devices) and destined for the internet. I can't figure out what the requirement is for matching ethernet frames in terms of which hook it can be in.

The wiki says:

" Do not forget that the layer 2 header information is only available in the input path."

Obviously I don't care so much about packets sent to the router, I'm interested in packets that will be routed, perhaps prerouting? I don't know what "the input path" really means..

hooks diagram is here:

I imagine prerouting, which comes before input, should be sufficiently early that the mac is known right?

1 Like

So, the answer is that yes prerouting seems to be early enough to have ether saddr matches and yes now I can easily block my kids tablets without having to turn off the entire wifi SSID, using a cfengine condition (does a certain file exist? then flush the set, otherwise add the kids devices to the set)

1 Like

Just to sneak peek into the firewall4 aspect of this thread, this is the nearly default firewall4 ruleset generated by the firmware. I did have some ipsets defined in the firewall section of luci.

If you want to start to plan ahead for custom rules using the firewall4 architecture, you would plan for your chain to be included within the table inet fw4 section by including a *.nft file in /etc/nftables.d.

fw4 print
# fw4 print

table inet fw4
flush table inet fw4

table inet fw4 {
        # Set definitions

        set bulk4 {
                type ipv4_addr
                timeout 86400s

        set bulk6 {
                type ipv6_addr
                timeout 86400s

        set besteffort4 {
                type ipv4_addr
                timeout 86400s

        set besteffort6 {
                type ipv6_addr
                timeout 86400s

        set video4 {
                type ipv4_addr
                timeout 86400s

        set video6 {
                type ipv6_addr
                timeout 86400s

        set voice4 {
                type ipv4_addr
                timeout 86400s

        set voice6 {
                type ipv6_addr
                timeout 86400s

        # Defines

        define lan_devices = { "eth0" }
        define lan_subnets = {, 2601:nnn:nnnn:nnnn::/60, fd8f:ffff:ffff::/60 }
        define wan_devices = { "eth1", "eth1" }
        define wan_subnets = { 68.nn.nnn.n/21, 2001:nnn:nnnn:nn:nnnn:nnnn:nnnn:nnnn }

        # User includes

        include "/etc/nftables.d/*.nft"

        # Filter rules

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

                iifname "lo" accept comment "!fw4: Accept traffic from loopback"

                ct state established,related accept comment "!fw4: Allow inbound established and related flows"
                tcp flags & (fin | syn | rst | ack) == syn jump syn_flood comment "!fw4: Rate limit TCP syn packets"
                iifname "eth0" jump input_lan comment "!fw4: Handle lan IPv4/IPv6 input traffic"
                iifname { "eth1", "eth1" } jump input_wan comment "!fw4: Handle wan IPv4/IPv6 input traffic"

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

                ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
                iifname "eth0" jump forward_lan comment "!fw4: Handle lan IPv4/IPv6 forward traffic"
                iifname { "eth1", "eth1" } jump forward_wan comment "!fw4: Handle wan IPv4/IPv6 forward traffic"
                jump handle_reject

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

                oifname "lo" accept comment "!fw4: Accept traffic towards loopback"

                ct state established,related accept comment "!fw4: Allow outbound established and related flows"
                oifname "eth0" jump output_lan comment "!fw4: Handle lan IPv4/IPv6 output traffic"
                oifname { "eth1", "eth1" } jump output_wan comment "!fw4: Handle wan IPv4/IPv6 output traffic"

        chain handle_reject {
                meta l4proto tcp reject with tcp reset comment "!fw4: Reject TCP traffic"
                reject with icmpx type port-unreachable comment "!fw4: Reject any other traffic"

        chain syn_flood {
                tcp flags & (fin | syn | rst | ack) == syn 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 {
                jump accept_to_wan comment "!fw4: Accept lan to wan forwarding"
                jump accept_to_lan

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

        chain accept_to_lan {
                oifname "eth0" 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"
                meta nfproto ipv4 icmp type 8 counter accept comment "!fw4: Allow-Ping"
                meta nfproto ipv4 meta l4proto igmp counter accept comment "!fw4: Allow-IGMP"
                ip6 saddr fc00::/6 ip6 daddr fc00::/6 udp dport 546 counter accept comment "!fw4: Allow-DHCPv6"
                ip6 saddr fe80::/10 icmpv6 type . icmpv6 code { 130 . 0, 131 . 0, 132 . 0, 143 . 0 } counter accept comment "!fw4: Allow-MLD"
                meta nfproto ipv6 icmpv6 type { 128, 129, 1, 3, 133, 134 } limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Input"
                meta nfproto ipv6 icmpv6 type . icmpv6 code { 2 . 0, 4 . 0, 4 . 1, 135 . 0, 136 . 0 } limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Input"
                jump reject_from_wan

        chain output_wan {
                jump accept_to_wan

        chain forward_wan {
                meta nfproto ipv6 icmpv6 type { 128, 129, 1, 3 } limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Forward"
                meta nfproto ipv6 icmpv6 type . icmpv6 code { 2 . 0, 4 . 0, 4 . 1 } limit rate 1000/second 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 {
                oifname { "eth1", "eth1" } counter accept comment "!fw4: accept wan IPv4/IPv6 traffic"

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

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

        # NAT rules

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

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

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

        # Raw rules (notrack & helper)

        chain raw_prerouting {
                type filter hook prerouting priority raw; policy accept;
                iifname "eth0" jump helper_lan comment "!fw4: lan IPv4/IPv6 CT helper assignment"

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

        chain helper_lan {

        # Mangle rules

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

        chain mangle_output {
                type filter hook output priority mangle; policy accept;

        chain mangle_forward {
                type filter hook forward priority mangle; policy accept;
                iifname { "eth1", "eth1" } tcp flags syn tcp option maxseg size set rt mtu comment "!fw4: Zone wan IPv4/IPv6 ingress MTU fixing"
                oifname { "eth1", "eth1" } tcp flags syn tcp option maxseg size set rt mtu comment "!fw4: Zone wan IPv4/IPv6 egress MTU fixing"

I had some stability issues with my custom compile, so I reverted back to 21.02.1, but I've been waiting to see how nftables will look out-of-the-box when firewall4 takes over.

1 Like

Why the duplicate interface names for several rules? It must be some magic trick I still need to learn :slight_smile:

wan and wan6.

How does that work? There is no need for meta nfproto to help differentiate given the same physical interface name?

Both interfaces are assigned to the wan firewall zone. I'm guessing this initial fw4 implementation won't represent a 100% native nftables approach, but a way to represent the existing firewall rules in nftables.

I'd like to see more verdict maps and concatenations.


Please feel free to suggest specific ruleset improvements (that can be algorithmically expressed and do not require human level intelligence a.k.a. "hand optimized" rules :slight_smile: )

Firewall4 was designed to be a template processor more or less so the templates that define the ruleset are located in /usr/share/firewall4/templates/ and can be edited on-target.

I'd welcome specific improvement suggestions. The current ruleset is basically a 1:1 translation of the firewall3 iptables rules with some little manual changes.


I will do my best to provide constructive feedback as I experiment more. My original comment about verdict maps and concatenations was probably misplaced since I meant it in the broad sense of all the nftables experiments in these forums.

My initial reaction is that fw4 shouldn’t create hooks with empty chains (e.g. NAT) until a rule is ready to be added. I don’t know how correct this hunch is, but I offer it nonetheless.

You do create some define expressions for devices and subnets but do not use them yet in the rules (or maybe just the default rules).

BTW, nft with json and terse options was supposedly fixed in a later nftables version, so you might be interested if you revisit function find_existing_sets().

I appreciate what you and stintel are doing to bring nftables to fruition!


any plan to have a nftables gui ? or will luci-firewall support nftables ?

Hi all - would anyone mind helping a fellow newby with trying out the new firewall4 package?


I am trying to build a new image (from source, master branch) with firewall4 included. In here I see the following package info:

define Package/firewall4
  CATEGORY:=Base system
  TITLE:=OpenWrt 4th gen firewall
  DEPENDS:=+ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus +kmod-nft-core +kmod-nft-fib +kmod-nft-nat +kmod-nft-nat6 +nftables-json
  CONFLICTS:=firewall kmod-ipt-nat

So naturally my first instinct was to modify the default configuration for my target with something like this:

# Remove kmod-ipt-nat conflict and set 'firewall3' to "N"
# Add 'firewall4' and all of the dependencies

Then I run make defconfig - hoping the above changes took place, alas - they do not. This is how it looks:

If I try setting firewall4 to 'Y' in make menuconfig - I see this:



Since I could only build the image where firewall4 is a module instead of built-in - what is the correct way to try switching from firewall3 to firewall4? Is the latter supposed to be installed on top of the former after building the image?

I tried my best with searching the forum and googling - but I did not find the answer.

Will appreciate your help!

P.S. Happy Holidays everyone!

See this discussion on github. You can build from stintel’s repo where the dependencies are fixed.


Thank you!

Thanks again for the link @dave14305

Successfully built and flashed an image with fw4+luci for Edgerouter X. Preserving the settings worked great, no modifications were needed whatsoever.

Stuff like Wireguard and SQM confirmed to work. The only change I had to do is changing usr/share/luci/menu.d/luci-app-firewall.json from /sbin/fw3 to /usr/sbin/fw3 for luci to work with fw4 as you had suggested in the PR.

If anyone here is interested, let me know and I can share the sysupgrade image.

1 Like

Quick clarification… this should read “from /sbin/fw3 to /usr/sbin/fw4“, yes?