Skip to main content
Redhat Developers  Logo
  • Products

    Featured

    • Red Hat Enterprise Linux
      Red Hat Enterprise Linux Icon
    • Red Hat OpenShift AI
      Red Hat OpenShift AI
    • Red Hat Enterprise Linux AI
      Linux icon inside of a brain
    • Image mode for Red Hat Enterprise Linux
      RHEL image mode
    • Red Hat OpenShift
      Openshift icon
    • Red Hat Ansible Automation Platform
      Ansible icon
    • Red Hat Developer Hub
      Developer Hub
    • View All Red Hat Products
    • Linux

      • Red Hat Enterprise Linux
      • Image mode for Red Hat Enterprise Linux
      • Red Hat Universal Base Images (UBI)
    • Java runtimes & frameworks

      • JBoss Enterprise Application Platform
      • Red Hat build of OpenJDK
    • Kubernetes

      • Red Hat OpenShift
      • Microsoft Azure Red Hat OpenShift
      • Red Hat OpenShift Virtualization
      • Red Hat OpenShift Lightspeed
    • Integration & App Connectivity

      • Red Hat Build of Apache Camel
      • Red Hat Service Interconnect
      • Red Hat Connectivity Link
    • AI/ML

      • Red Hat OpenShift AI
      • Red Hat Enterprise Linux AI
    • Automation

      • Red Hat Ansible Automation Platform
      • Red Hat Ansible Lightspeed
    • Developer tools

      • Red Hat Trusted Software Supply Chain
      • Podman Desktop
      • Red Hat OpenShift Dev Spaces
    • Developer Sandbox

      Developer Sandbox
      Try Red Hat products and technologies without setup or configuration fees for 30 days with this shared Openshift and Kubernetes cluster.
    • Try at no cost
  • Technologies

    Featured

    • AI/ML
      AI/ML Icon
    • Linux
      Linux Icon
    • Kubernetes
      Cloud icon
    • Automation
      Automation Icon showing arrows moving in a circle around a gear
    • View All Technologies
    • Programming Languages & Frameworks

      • Java
      • Python
      • JavaScript
    • System Design & Architecture

      • Red Hat architecture and design patterns
      • Microservices
      • Event-Driven Architecture
      • Databases
    • Developer Productivity

      • Developer productivity
      • Developer Tools
      • GitOps
    • Secure Development & Architectures

      • Security
      • Secure coding
    • Platform Engineering

      • DevOps
      • DevSecOps
      • Ansible automation for applications and services
    • Automated Data Processing

      • AI/ML
      • Data Science
      • Apache Kafka on Kubernetes
      • View All Technologies
    • Start exploring in the Developer Sandbox for free

      sandbox graphic
      Try Red Hat's products and technologies without setup or configuration.
    • Try at no cost
  • Learn

    Featured

    • Kubernetes & Cloud Native
      Openshift icon
    • Linux
      Rhel icon
    • Automation
      Ansible cloud icon
    • Java
      Java icon
    • AI/ML
      AI/ML Icon
    • View All Learning Resources

    E-Books

    • GitOps Cookbook
    • Podman in Action
    • Kubernetes Operators
    • The Path to GitOps
    • View All E-books

    Cheat Sheets

    • Linux Commands
    • Bash Commands
    • Git
    • systemd Commands
    • View All Cheat Sheets

    Documentation

    • API Catalog
    • Product Documentation
    • Legacy Documentation
    • Red Hat Learning

      Learning image
      Boost your technical skills to expert-level with the help of interactive lessons offered by various Red Hat Learning programs.
    • Explore Red Hat Learning
  • Developer Sandbox

    Developer Sandbox

    • Access Red Hat’s products and technologies without setup or configuration, and start developing quicker than ever before with our new, no-cost sandbox environments.
    • Explore Developer Sandbox

    Featured Developer Sandbox activities

    • Get started with your Developer Sandbox
    • OpenShift virtualization and application modernization using the Developer Sandbox
    • Explore all Developer Sandbox activities

    Ready to start developing apps?

    • Try at no cost
  • Blog
  • Events
  • Videos

Migrating my iptables setup to nftables

January 10, 2017
Phil Sutter
Related topics:
SecurityDevOps

Share:

    Wanting to become familiar with nftables, I decided to jump in at the deep end and just use it on my local workstation. The goal was to replace the existing iptables setup, ideally without any drawbacks. The following essay will guide you through what I have done in order to achieve that.

    In order to be able to follow, you should already be familiar with iptables and at least have a rough idea of what nftables are. I don't see much sense in reading the following text without already being an iptables user, but here is a recommendation of resources to consult regarding nftables (although it's not strictly a must-have):

    • LWN has a nice introductory article.
    • On Linux Audit there is a short comparison between iptables and nftables.
    • Pablo Neira Ayuso's excellent nftables beginner workshop is available on YouTube.
    • Finally there is Florian Westphal's talk in which he dives deeply into the technical reasons why iptables is being replaced and why nftables is such a good substitute.

    I don't rely upon distributions facilities to manage firewall setups so much but rather have my own shell script, which provides a bit more convenience especially when it comes to keeping iptables and ip6tables in sync. This is how it looks:

    #!/bin/bash
    
    # useful wrappers for failure analysis
    function cmd_or_print() { # command
    	"$@" || echo "failed at: '$@'"
    }
    function ipt() { # iptables params
    	cmd_or_print iptables "$@"
    }
    function ip6t { # ip6tables params
    	cmd_or_print ip6tables "$@"
    }
    
    # have a simple way of doing things in iptables and ip6tables in parallel
    function ip46t() { # ip(6)tables params
    	ipt "$@"
    	ip6t "$@"
    }
    
    # interfaces
    wan=eth0
    wan6=eth0
    #vpn=tap23
    vpn=tun+
    #vpn=tap6
    nstx=tun0
    sixxs=sixxs0
    
    # ip addresses
    sixxs_pop="212.224.0.188"
    my_ipv6_pfx="2001:41d0:8:c8e::/64"
    my_second_ipv4="85.214.71.2"
    
    # clear out everything
    for it in iptables ip6tables; do
    for table in filter mangle nat raw; do
    	$it -t $table -nL >/dev/null 2>&1 || continue # non-existing table
    
    	$it -t $table -F		# delete rules
    	$it -t $table -X		# delete custom chains
    	$it -t $table -Z		# zero counters
    done
    done
    
    ### define custom chains
    
    # filter invalid packets
    ip46t -N invalid
    ip46t -A invalid -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
    ip46t -A invalid -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
    ip46t -N blacklist
    ip46t -A invalid -j blacklist
    
    # stateful firewalling if everything else fails
    ip46t -N states
    ip46t -A states -m state --state INVALID -j REJECT
    ip46t -A states -m state --state RELATED,ESTABLISHED -j ACCEPT
    
    # filtering input on the wan interface
    ip46t -N wan_in
    ipt -A wan_in -p icmp -j ACCEPT
    ip6t -A wan_in -p icmpv6 -j ACCEPT
    ip6t -A wan_in -m state --state NEW -m udp -p udp --dport 546 --sport 547 \
    				-s fe80::/10 -d fe80::/10 -j ACCEPT # dhcpv6
    ip46t -A wan_in -p tcp --dport 22 -j ACCEPT # ssh
    ip46t -A wan_in -p esp -j ACCEPT # ipsec
    ipt -A wan_in -p ah -j ACCEPT # ipsec
    ip6t -A wan_in -m ah -j ACCEPT # ipsec
    ip46t -A wan_in -p tcp --dport 27374:27474 -j ACCEPT # super hidden port range
    ip46t -A wan_in -p udp --dport 27374:27474 -j ACCEPT # super hidden port range
    ipt -A wan_in -p ipv6 -j ACCEPT # allow ipv6 traffic, handled by ip6tables
    
    # set policy of chains to DROP
    for it in iptables ip6tables; do
    for chain in INPUT OUTPUT FORWARD; do
    	$it -P $chain DROP
    done
    done
    
    # builtin chains
    
    ip46t -A INPUT -j invalid
    ip46t -A INPUT -j wan_in
    ip46t -A INPUT -i lo -j ACCEPT
    ip46t -A INPUT -i vnetbr0 -j ACCEPT
    ip46t -A INPUT -j states
    
    ip46t -A OUTPUT -j ACCEPT
    
    ip46t -A FORWARD -i vnetbr0 -j ACCEPT
    ip46t -A FORWARD -o vnetbr0 -j wan_in
    ip46t -A FORWARD -o vnetbr0 -j states
    
    ipt -t nat -A POSTROUTING -s 192.168.42.0/24 \! -o vnetbr0 -j MASQUERADE

    Since it clears the whole table setup at first, after making changes I can simply fire it up again and then tell the distribution to save the rules so they are applied automatically during system boot up. Here is the ip(6)tables-save output after having it applied:

    *nat
    :PREROUTING ACCEPT [0:0]
    :INPUT ACCEPT [0:0]
    :OUTPUT ACCEPT [2:121]
    :POSTROUTING ACCEPT [2:121]
    [0:0] -A POSTROUTING -s 192.168.42.0/24 ! -o vnetbr0 -j MASQUERADE
    COMMIT
    *raw
    :PREROUTING ACCEPT [21:5125]
    :OUTPUT ACCEPT [22:2332]
    COMMIT
    *mangle
    :PREROUTING ACCEPT [23:5414]
    :INPUT ACCEPT [23:5414]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [22:2332]
    :POSTROUTING ACCEPT [21:2280]
    COMMIT
    *filter
    :INPUT DROP [0:0]
    :FORWARD DROP [0:0]
    :OUTPUT DROP [0:0]
    :blacklist - [0:0]
    :invalid - [0:0]
    :states - [0:0]
    :wan_in - [0:0]
    [21:5125] -A INPUT -j invalid
    [21:5125] -A INPUT -j wan_in
    [0:0] -A INPUT -i lo -j ACCEPT
    [1:80] -A INPUT -i vnetbr0 -j ACCEPT
    [20:5045] -A INPUT -j states
    [0:0] -A FORWARD -i vnetbr0 -j ACCEPT
    [0:0] -A FORWARD -o vnetbr0 -j wan_in
    [0:0] -A FORWARD -o vnetbr0 -j states
    [21:2280] -A OUTPUT -j ACCEPT
    [0:0] -A invalid -p tcp -m tcp --tcp-flags FIN,SYN FIN,SYN -j DROP
    [0:0] -A invalid -p tcp -m tcp --tcp-flags SYN,RST SYN,RST -j DROP
    [21:5125] -A invalid -j blacklist
    [0:0] -A states -m state --state INVALID -j REJECT \
    			--reject-with icmp-port-unreachable
    [20:5045] -A states -m state --state RELATED,ESTABLISHED -j ACCEPT
    [0:0] -A wan_in -p icmp -j ACCEPT
    [0:0] -A wan_in -p tcp -m tcp --dport 22 -j ACCEPT
    [0:0] -A wan_in -p esp -j ACCEPT
    [0:0] -A wan_in -p ah -j ACCEPT
    [0:0] -A wan_in -p tcp -m tcp --dport 27374:27474 -j ACCEPT
    [0:0] -A wan_in -p udp -m udp --dport 27374:27474 -j ACCEPT
    [0:0] -A wan_in -p ipv6 -j ACCEPT
    COMMIT
    *filter
    :INPUT DROP [0:0]
    :FORWARD DROP [0:0]
    :OUTPUT DROP [0:0]
    :blacklist - [0:0]
    :invalid - [0:0]
    :states - [0:0]
    :wan_in - [0:0]
    [23:2255] -A INPUT -j invalid
    [23:2255] -A INPUT -j wan_in
    [18:1741] -A INPUT -i lo -j ACCEPT
    [0:0] -A INPUT -i vnetbr0 -j ACCEPT
    [4:450] -A INPUT -j states
    [0:0] -A FORWARD -i vnetbr0 -j ACCEPT
    [0:0] -A FORWARD -o vnetbr0 -j wan_in
    [0:0] -A FORWARD -o vnetbr0 -j states
    [26:2482] -A OUTPUT -j ACCEPT
    [0:0] -A invalid -p tcp -m tcp --tcp-flags FIN,SYN FIN,SYN -j DROP
    [0:0] -A invalid -p tcp -m tcp --tcp-flags SYN,RST SYN,RST -j DROP
    [23:2255] -A invalid -j blacklist
    [0:0] -A states -m state --state INVALID -j REJECT \
    			--reject-with icmp6-port-unreachable
    [4:450] -A states -m state --state RELATED,ESTABLISHED -j ACCEPT
    [1:64] -A wan_in -p ipv6-icmp -j ACCEPT
    [0:0] -A wan_in -s fe80::/10 -d fe80::/10 -p udp -m state --state NEW \
    				-m udp --sport 547 --dport 546 -j ACCEPT
    [0:0] -A wan_in -p tcp -m tcp --dport 22 -j ACCEPT
    [0:0] -A wan_in -p esp -j ACCEPT
    [0:0] -A wan_in -m ah -j ACCEPT
    [0:0] -A wan_in -p tcp -m tcp --dport 27374:27474 -j ACCEPT
    [0:0] -A wan_in -p udp -m udp --dport 27374:27474 -j ACCEPT
    COMMIT

    Using the converter as a starting point

    Since mid of February 2016, the iptables repository contains a command ip(6)tables-restore-translate (still unreleased) to read from iptables-save output and generate a suitable nftables setup. In case it fails to translate a given statement into nftables syntax, it will output the problematic line as a comment. Sensing some relief from the tedious work of finding a suitable replacement for each and every rule, I decided to give it a go and continue with whatever it generates. This turned out to be a pretty good choice, considering the amount of work it saved me:

    add table ip nat
    add chain ip nat PREROUTING { type filter hook prerouting priority 0; }
    add chain ip nat INPUT { type filter hook input priority 0; }
    add chain ip nat OUTPUT { type filter hook output priority 0; }
    add chain ip nat POSTROUTING { type filter hook postrouting priority 0; }
    add rule ip nat POSTROUTING oifname != vnetbr0 ip saddr 192.168.42.0 counter masquerade
    add table ip raw
    add chain ip raw PREROUTING { type filter hook prerouting priority 0; }
    add chain ip raw OUTPUT { type filter hook output priority 0; }
    add table ip mangle
    add chain ip mangle PREROUTING { type filter hook prerouting priority 0; }
    add chain ip mangle INPUT { type filter hook input priority 0; }
    add chain ip mangle FORWARD { type filter hook forward priority 0; }
    add chain ip mangle OUTPUT { type filter hook output priority 0; }
    add chain ip mangle POSTROUTING { type filter hook postrouting priority 0; }
    add table ip filter
    add chain ip filter INPUT { type filter hook input priority 0; }
    add chain ip filter FORWARD { type filter hook forward priority 0; }
    add chain ip filter OUTPUT { type filter hook output priority 0; }
    add chain ip filter blacklist
    add chain ip filter invalid
    add chain ip filter states
    add chain ip filter wan_in
    add rule ip filter INPUT counter jump invalid
    add rule ip filter INPUT counter jump wan_in
    add rule ip filter INPUT iifname lo counter accept
    add rule ip filter INPUT iifname vnetbr0 counter accept
    add rule ip filter INPUT counter jump states
    add rule ip filter FORWARD iifname vnetbr0 counter accept
    add rule ip filter FORWARD oifname vnetbr0 counter jump wan_in
    add rule ip filter FORWARD oifname vnetbr0 counter jump states
    add rule ip filter OUTPUT counter accept
    add rule ip filter invalid tcp flags & fin|syn == fin|syn counter drop
    add rule ip filter invalid tcp flags & syn|rst == syn|rst counter drop
    add rule ip filter invalid counter jump blacklist
    add rule ip filter states ct state invalid counter reject
    add rule ip filter states ct state related,established counter accept
    add rule ip filter wan_in ip protocol icmp counter accept
    add rule ip filter wan_in tcp dport 22 counter accept
    add rule ip filter wan_in ip protocol esp counter accept
    add rule ip filter wan_in ip protocol ah counter accept
    add rule ip filter wan_in tcp dport 27374-27474 counter accept
    add rule ip filter wan_in udp dport 27374-27474 counter accept
    add rule ip filter wan_in ip protocol ipv6 counter accept
    add table ip6 filter
    add chain ip6 filter INPUT { type filter hook input priority 0; }
    add chain ip6 filter FORWARD { type filter hook forward priority 0; }
    add chain ip6 filter OUTPUT { type filter hook output priority 0; }
    add chain ip6 filter blacklist
    add chain ip6 filter invalid
    add chain ip6 filter states
    add chain ip6 filter wan_in
    add rule ip6 filter INPUT counter jump invalid
    add rule ip6 filter INPUT counter jump wan_in
    add rule ip6 filter INPUT iifname lo counter accept
    add rule ip6 filter INPUT iifname vnetbr0 counter accept
    add rule ip6 filter INPUT counter jump states
    add rule ip6 filter FORWARD iifname vnetbr0 counter accept
    add rule ip6 filter FORWARD oifname vnetbr0 counter jump wan_in
    add rule ip6 filter FORWARD oifname vnetbr0 counter jump states
    add rule ip6 filter OUTPUT counter accept
    add rule ip6 filter invalid tcp flags & fin|syn == fin|syn counter drop
    add rule ip6 filter invalid tcp flags & syn|rst == syn|rst counter drop
    add rule ip6 filter invalid counter jump blacklist
    add rule ip6 filter states ct state invalid  counter reject
    add rule ip6 filter states ct state related,established  counter accept
    add rule ip6 filter wan_in meta l4proto ipv6-icmp counter accept
    add rule ip6 filter wan_in ip6 saddr fe80:: ip6 daddr fe80:: \
    		ct state new udp sport 547 udp dport 546 counter accept
    add rule ip6 filter wan_in tcp dport 22 counter accept
    add rule ip6 filter wan_in meta l4proto esp counter accept
    add rule ip6 filter wan_in  counter accept
    add rule ip6 filter wan_in tcp dport 27374-27474 counter accept
    add rule ip6 filter wan_in udp dport 27374-27474 counter accept

    On a closer look, things turned out to be less than perfect even though there were no rules for which the converter failed to find a substitute. Here is a list of issues I found; together with how I solved them:

    Missing drop policy

    The converter apparently ignored the drop policy of all the built-in chains, effectively annulling the whole firewall. Luckily, this is easy to fix in hindsight: Just add the missing policy statement to the relevant chains like so:

    add chain ip filter INPUT { type filter hook input priority 0; policy drop; }

    Fixing the converter was easy, as explained in the patch's description.

    Dropped -m ah match

    It seems like the converter assumed an Authentication Header match without parameters like I used in ip6tables setup would never fail, and so I simply dropped it. This is not true though, as it is the suggested replacement for iptables' AH protocol match (i.e. -p ah). Looking at the equivalent ESP header match, I assumed that the following would be a valid replacement:

    nft add rule ip6 filter wan_in meta l4proto ah counter accept

    Sadly, this is not completely correct, either: In fact it matches only if the AH header follows immediately after the IPv6 header - if there is any other extension header in between, it does not see it. To my surprise, there is no easy fix - matching on mere existence of an IPv6 extension header is actually a limitation to nftables, and at the time of this writing there was no implementation available. So the l4proto match above had to suffice for now, luckily IPv6 extension headers don't see much use yet (other than AH and ESP, of course).

    Missing prefix lengths in subnet definitions

    I completely overlooked this and only found out about it when testing the results: The converter turned all subnet definitions into simple IP addresses, which then never matched (since those ending in zero are not used by hosts). Using the iptables-translate utility is an easy way of reproducing the issue:

    $ iptables-translate -A INPUT -s 10.0.0.0/8 -j ACCEPT
    nft add rule ip filter INPUT ip saddr 10.0.0.0 counter accept

    Luckily, this was quite as easy to fix as the missing policy statement above.

    TCP flags matches turned into a mess

    This problem is not visible from looking only at the generated nftables rules, but becomes obvious when listing the ruleset after having it applied: The rule's path to the kernel and back turns it from this:

    tcp flags & fin|syn == fin|syn counter drop

    into that:

    tcp flags & (fin | syn) | syn == fin | syn

    This is clearly a problem of the converter, which ignored the fact that binary AND precedes binary OR. The output of nftables does not seem like a mathematically correct transformation, either. But instead of trying to fix nftables in that aspect, I guess it would make more sense to have the converter generate the missing parentheses around the binary OR on the left side of the relational expression (see patch here and on top of that, I patched nftables' parser to allow extra (though unnecessary) parentheses around the right side.

    Wrong type of chains in nat table

    The replacement for iptables' NAT table was generated with chains of type filter, which is apparently wrong: The kernel will reject adding a rule with verdict masquerade to them. Instead, the type has to be set to nat to allow for that verdict. Luckily, this wasn't hard to fix, either.

    Optimizing the results

    While reviewing the remaining converter output, a few optimizations struck:

    • There is no need to keep empty chains (unless their policy diverts from the default of accept). And if a table becomes empty after removing all empty chains it contains, the whole table can be dropped.
    • The loop in updateipt.sh setting all built-in chain's policy to DROP made it necessary to explicitly allow outgoing traffic in OUTPUT chains. By keeping the chain's policy at the default, the explicit rule can go away and in turn the whole chain becomes empty so the above point applies here, as well.
    • There is no need to have a counter for every rule. In fact, most rules are totally uninteresting, so use counters only for rules, which should not see traffic (to spot problems or attacks) or are used for statistics/accounting (of which there are none in my setup).

    After applying the things from above and concatenating the rules for IPv4 and IPv6 (the single nft utility handles both), here's the result:

    add table ip nat
    add chain ip nat POSTROUTING { type nat hook postrouting priority 0; policy accept; }
    add rule ip nat POSTROUTING oifname != vnetbr0 ip saddr 192.168.42.0/24 masquerade
    add table ip filter
    add chain ip filter INPUT { type filter hook input priority 0; policy drop; }
    add chain ip filter FORWARD { type filter hook forward priority 0; policy drop; }
    add chain ip filter blacklist
    add chain ip filter invalid
    add chain ip filter states
    add chain ip filter wan_in
    add rule ip filter INPUT jump invalid
    add rule ip filter INPUT jump wan_in
    add rule ip filter INPUT iifname lo accept
    add rule ip filter INPUT iifname vnetbr0 accept
    add rule ip filter INPUT jump states
    add rule ip filter FORWARD iifname vnetbr0 accept
    add rule ip filter FORWARD oifname vnetbr0 jump wan_in
    add rule ip filter FORWARD oifname vnetbr0 jump states
    add rule ip filter invalid tcp flags & (fin|syn) == fin|syn counter drop
    add rule ip filter invalid tcp flags & (syn|rst) == syn|rst counter drop
    add rule ip filter invalid jump blacklist
    add rule ip filter states ct state invalid counter reject
    add rule ip filter states ct state related,established accept
    add rule ip filter wan_in ip protocol icmp accept
    add rule ip filter wan_in tcp dport 22 accept
    add rule ip filter wan_in ip protocol esp accept
    add rule ip filter wan_in ip protocol ah accept
    add rule ip filter wan_in tcp dport 27374-27474 accept
    add rule ip filter wan_in udp dport 27374-27474 accept
    add rule ip filter wan_in ip protocol ipv6 accept
    add table ip6 filter
    add chain ip6 filter INPUT { type filter hook input priority 0; policy drop; }
    add chain ip6 filter FORWARD { type filter hook forward priority 0; policy drop; }
    add chain ip6 filter blacklist
    add chain ip6 filter invalid
    add chain ip6 filter states
    add chain ip6 filter wan_in
    add rule ip6 filter INPUT jump invalid
    add rule ip6 filter INPUT jump wan_in
    add rule ip6 filter INPUT iifname lo accept
    add rule ip6 filter INPUT iifname vnetbr0 accept
    add rule ip6 filter INPUT jump states
    add rule ip6 filter FORWARD iifname vnetbr0 accept
    add rule ip6 filter FORWARD iifname vnetbr* oifname vnetbr* accept
    add rule ip6 filter FORWARD oifname vnetbr0 jump wan_in
    add rule ip6 filter FORWARD oifname vnetbr0 jump states
    add rule ip6 filter invalid tcp flags & (fin|syn) == fin|syn counter drop
    add rule ip6 filter invalid tcp flags & (syn|rst) == syn|rst counter drop
    add rule ip6 filter invalid jump blacklist
    add rule ip6 filter states ct state invalid counter reject
    add rule ip6 filter states ct state related,established accept
    add rule ip6 filter wan_in meta l4proto ipv6-icmp accept
    add rule ip6 filter wan_in ip6 saddr fe80::/10 ip6 daddr fe80::/10 \
    			ct state new udp sport 547 udp dport 546 accept
    add rule ip6 filter wan_in tcp dport 22 accept
    add rule ip6 filter wan_in meta l4proto esp accept
    add rule ip6 filter wan_in meta l4proto ah accept
    add rule ip6 filter wan_in tcp dport 27374-27474 accept
    add rule ip6 filter wan_in udp dport 27374-27474 accept

    Making use of the new inet table

    Now for some real feature of nftables, namely handling of IPv4 and IPv6 traffic in a common table. For instance, the FORWARD chains above are exactly identical since there are no IP version specific rules in them. The only problem with consolidating them is that they reference wan_in and states chains and since the INPUT chain uses these as well, this ends up in an all or nothing approach for the whole filter tables. But in this case it still made sense since the tables were so similar. So one by one, first the FORWARD chain:

    add table inet filter
    add chain inet filter FORWARD { type filter hook forward priority 0; policy drop; }
    add rule inet filter FORWARD iifname vnetbr0 accept
    add rule inet filter FORWARD oifname vnetbr0 jump wan_in
    add rule inet filter FORWARD oifname vnetbr0 jump states

    Next the wan_in and states chains it depends upon:

    add chain inet filter wan_in
    add rule inet filter wan_in ip protocol icmp accept
    add rule inet filter wan_in meta l4proto ipv6-icmp accept
    add rule inet filter wan_in ip6 saddr fe80:: ip6 daddr fe80:: \
    		ct state new  udp sport 547 udp dport 546 accept
    add rule inet filter wan_in tcp dport 22 accept
    add rule inet filter wan_in ip protocol esp accept
    add rule inet filter wan_in meta l4proto esp accept
    add rule inet filter wan_in ip protocol ah accept
    add rule inet filter wan_in meta l4proto ah accept
    add rule inet filter wan_in tcp dport 27374-27474 accept
    add rule inet filter wan_in udp dport 27374-27474 accept
    add rule inet filter wan_in ip protocol ipv6 accept
    add chain inet filter states
    add rule inet filter states ct state invalid counter reject
    add rule inet filter states ct state related,established accept

    Finally the INPUT chain and its dependency, the invalid chain (which in turn jumps to the empty blacklist):

    add chain inet filter INPUT { type filter hook input priority 0; policy drop; }
    add rule inet filter INPUT jump invalid
    add rule inet filter INPUT jump wan_in
    add rule inet filter INPUT iifname lo accept
    add rule inet filter INPUT iifname vnetbr0 accept
    add rule inet filter INPUT jump states
    add chain inet filter invalid
    add chain inet filter blacklist
    add rule inet filter invalid tcp flags & (fin|syn) == fin|syn counter drop
    add rule inet filter invalid tcp flags & (syn|rst) == syn|rst counter drop
    add rule inet filter invalid jump blacklist

    Of course, the statements above need to be reordered a bit so chains are created prior to rules referencing them.

    Using the blacklist chain - or an alternative

    The empty chain blacklist is meant to be managed by an application layer IPS such as fail2ban. Adjusting that, is beyond the scope of this document, but here are example rules for adding IPv4/IPv6 addresses to it:

    add rule inet filter blacklist ip saddr 192.168.42.3 drop
    add rule inet filter blacklist ip6 saddr feed:babe::3 drop

    In nftables world though, this is considered inelegant and one would rather use named sets instead. To do so, two sets need to be created since a single one can't hold addresses of different families:

    add set inet filter blacklist4 { type ipv4_addr; }
    add set inet filter blacklist6 { type ipv6_addr; }

    Next, drop rules are created in invalid chain (since the IPS may work with the named sets as a full replacement to a dedicated chain, which includes clearing them completely):

    add rule inet filter invalid ip saddr @blacklist4 drop
    add rule inet filter invalid ip6 saddr @blacklist6 drop

    Adding IP addresses to the sets then works like so:

    add element inet filter blacklist4 { 192.168.42.3 }
    add element inet filter blacklist6 { feed:babe::3 }

    About meta l4proto and unnamed sets

    While testing correct functionality of the generated rules in inet family tables, it showed that meta l4proto matches are actually IP address family agnostic, so the IPv4 specific ones (i.e. ip protocol) may be dropped altogether. Since there are multiple meta l4proto matches in wan_in chain, it makes sense to combine them using an anonymous set like so:

    add rule inet filter wan_in meta l4proto { icmp, ipv6-icmp, esp, ah } accept

    Rule set dump file formats

    In order to load a rule set, nft program supports reading from a file with some flexibility in its syntax. On one hand, just adding the quoted statements above works just fine. Though nft is also able to read a format just as nft list ruleset displays, which might be a little clearer to read since it puts tables, chains, and rules in relation. Here is what the full rule set with all mentioned modifications looks like in "nested" representation:

    table ip nat {
    	chain POSTROUTING {
    		type nat hook postrouting priority 0
    		policy accept
    
    		oifname != vnetbr0 ip saddr 192.168.42.0/24 masquerade
    	}
    }
    table inet filter {
    	set blacklist4 {
    		type ipv4_addr
    	}
    	set blacklist6 {
    		type ipv6_addr
    	}
    	chain INPUT {
    		type filter hook input priority 0
    		policy drop
    
    		jump invalid
    		jump wan_in
    		iifname lo accept
    		iifname vnetbr0 accept
    		jump states
    	}
    	chain FORWARD {
    		type filter hook forward priority 0
    		policy drop
    
    		iifname vnetbr0 accept
    		oifname vnetbr0 jump wan_in
    		oifname vnetbr0 jump states
    	}
    	chain wan_in {
    		ip6 saddr fe80:: ip6 daddr fe80:: \
    			ct state new udp sport 547 udp dport 546 accept
    		meta l4proto { icmp, ipv6-icmp, esp, ah } accept
    		tcp dport 22 accept
    		tcp dport 27374-27474 accept
    		udp dport 27374-27474 accept
    		ip protocol ipv6 accept
    	}
    	chain states {
    		ct state invalid counter reject
    		ct state related,established accept
    	}
    	chain invalid {
    		tcp flags & (fin|syn) == fin|syn counter drop
    		tcp flags & (syn|rst) == syn|rst counter drop
    		ip saddr @blacklist4 drop
    		ip6 saddr @blacklist6 drop
    	}
    }

    Depending on personal preference, this may be decorated with comments (starting with the commonly used hash (#) character) as well. Like with iptables, it is also possible to add comments to rules, which have the benefit of being part of nft list ruleset output and therefore are present in whatever form rules are managed. The only requirement is that they appear at the end of the rule specification, and when entered on command line quotes have to be escaped:

    add rule inet filter INPUT counter comment "useless rule having a comment"

    Finishing up

    So, satisfied with the outcome of my rule set conversion and optimization, the very last piece to complete the puzzle was to create a shell script equivalent to updateipt.sh I started with. Given that I do not need any shell tricks anymore as well as there is support for comments, I can use nft directly as script interpreter:

    #!/sbin/nft -f

    When it comes to clearing the tables (or rather ruleset in this case) at the beginning, I found out that you could also mix the syntaxes described above. So one can simply add the following line as the first command before the actual table definitions:

    flush ruleset

    This is much easier than the common -F, -X, -Z dance for all tables of both iptables and ip6tables!

    Last updated: January 9, 2017

    Recent Posts

    • How Trilio secures OpenShift virtual machines and containers

    • How to implement observability with Node.js and Llama Stack

    • How to encrypt RHEL images for Azure confidential VMs

    • How to manage RHEL virtual machines with Podman Desktop

    • Speech-to-text with Whisper and Red Hat AI Inference Server

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Products

    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform

    Build

    • Developer Sandbox
    • Developer Tools
    • Interactive Tutorials
    • API Catalog

    Quicklinks

    • Learning Resources
    • E-books
    • Cheat Sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site Status Dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Report a website issue