In Red Hat Enterprise Linux 8 the preferred low level firewall solution is nftables. This post is an introduction to using nftables. This is most relevant for system administrators and DevOps practitioners. Where it makes sense we will highlight differences between nftables and its predecessor iptables.

Firstly, it must be stated that nftables is both a userland utility, nft, and a kernel subsystem. Inside the kernel it builds upon the kernel’s netfilter subsystem. In this post we will focus on the user facing nft utility.

This post includes command examples that the reader can follow along with on a test machine. This may be useful to gain a better understanding of the content.

Note: In this post some of the command output may be long. As such, we ellipse ... the irrelevant or unimportant parts of output.

Getting Started

So what does the default setup of nftables look like? Let’s find out by listing the entire rule set.

# nft list ruleset

Which results in... nothing. Well, that’s not very exciting. What gives?

By default, nftables does not pre-create tables and chains like its predecessor iptables. An empty ruleset has zero resource cost. Compare that to iptables where each pre-created table and chain must be considered and base packet counters incremented⁠—even if they’re empty.

Creating Tables

In nftables you need to manually create tables. Tables need to qualify a family; ip, ip6, inet, arp, bridge, or netdev. inet means the table will process both ipv4 and ipv6 packets. It’s the family we’ll use throughout this post.

Note: For those coming from iptables, the term table may be a bit confusing. In nftables a table is simply a namespace⁠—nothing more, nothing less. It’s a collection of chains, rules, and sets, and other objects.

Let’s create our first table and list the rule set.

# nft add table inet my_table
# nft list ruleset
table inet my_table {
}

So now we have a table, but by itself it won’t do much. Let’s move on to chains.

Creating Chains

Chains are the objects that will contain our firewall rules.

Just like tables, chains need to be explicitly created. When creating the chain you need to specify what table the chain belongs to as well as the type, the hook, and the priority. For this introduction we’ll keep things simple by using filter, input, and priority 0 to filter packets destined to the host.

# nft add chain inet my_table my_filter_chain { type filter hook input priority 0 \; }

Note: The backslash (\) is necessary so the shell doesn’t interpret the semicolon as the end of the command.

Chains can also be created without specifying a hook. Chains created this way are equivalent to iptables user defined chains. Rules can use the jump or goto statements to execute rules in the chain. This is useful to logically separate rules or to share a subset of rules that would otherwise be duplicated.

# nft add chain inet my_table my_utility_chain

Creating Rules

Now that you’ve created a table and a chain you can finally add some firewall rules. Let’s add a rule to accept SSH.

# nft add rule inet my_table my_filter_chain tcp dport ssh accept

One thing to note here is that since we added this to a table of the inet family a single rule will process both IPv4 and IPv6 packets.

The add verb will append the rule to the end of the chain. You can also use the insert verb which will prepend the rule to the head of the chain.

# nft insert rule inet my_table my_filter_chain tcp dport http accept

Having added two rules, let’s look at what the ruleset looks like.

# nft list ruleset
table inet my_table {
    chain my_filter_chain {
    type filter hook input priority 0; policy accept;
    tcp dport http accept
    tcp dport ssh accept
    }
}

Note that the http rule occurs before the ssh rule because we used the insert verb above.

You can also add a rule at an arbitrary location in a chain. There are two ways to do this.

  1. Use index to specify an index into the list of rules. Using add will insert the new rule after the rule at the given index. Using insert will insert the new rule before the rule at the given index. index values start at 0.

# nft insert rule inet my_table my_filter_chain index 1 tcp dport nfs accept
# nft list ruleset
table inet my_table {
    chain my_filter_chain {
    type filter hook input priority 0; policy accept;
    tcp dport http accept
    tcp dport nfs accept
    tcp dport ssh accept
    }
}

# nft add rule inet my_table my_filter_chain index 0 tcp dport 1234 accept
# nft list ruleset
table inet my_table {
    chain my_filter_chain {
    type filter hook input priority 0; policy accept;
    tcp dport http accept
    tcp dport 1234 accept
    tcp dport nfs accept
    tcp dport ssh accept
    }
}

Note: Using index with the insert verb is mostly equivalent to iptables -I option with an index. The first caveat is nftables index values start at 0. The second caveat is index has to refer to an existing rule. This means "nft insert rule … index 0" on an empty chain in invalid.

  1. Use handle to specify the rule to insert the rule after or before. To insert after use the add verb. To insert before use the insert verb. You can get rule handles with the –handle option when listing rules.

# nft --handle list ruleset
table inet my_table { # handle 21
    chain my_filter_chain { # handle 1
    type filter hook input priority 0; policy accept;
    tcp dport http accept # handle 3
    tcp dport ssh accept # handle 2
    }
}
# nft add rule inet my_table my_filter_chain handle 3 tcp dport 1234 accept
# nft insert rule inet my_table my_filter_chain handle 2 tcp dport nfs accept
# nft --handle list ruleset
table inet my_table { # handle 21
    chain my_filter_chain { # handle 1
    type filter hook input priority 0; policy accept;
    tcp dport http accept # handle 3
    tcp dport 1234 accept # handle 8
    tcp dport nfs accept # handle 7
    tcp dport ssh accept # handle 2
    }
}

In nftables a rule handle is stable and will not change until the rule is deleted. This gives a stable reference to the rule without having to rely on an index, which may change if another rule is inserted.

You can also get the rule handle at the time of creation by using both the –echo and –handle options. The rule will be echoed back to the CLI with the handle included.

# nft --echo --handle add rule inet my_table my_filter_chain udp dport 3333 accept
add rule inet my_table my_filter_chain udp dport 3333 accept # handle 4

Note: Older version of nftables used the keyword position. This keyword has since been deprecated in favor of handle.

Deleting Rules

Deleting rules is done by using the rule handle similar to the add and insert commands above.

The first step is to find the handle of the rule you want to delete.

# nft --handle list ruleset
table inet my_table { # handle 21
    chain my_filter_chain { # handle 1
    type filter hook input priority 0; policy accept;
    tcp dport http accept # handle 3
    tcp dport 1234 accept # handle 8
    tcp dport nfs accept # handle 7
    tcp dport ssh accept # handle 2
    }
}

Then use the handle to delete the rule.

# nft delete rule inet my_table my_filter_chain handle 8
# nft --handle list ruleset
table inet my_table { # handle 21
    chain my_filter_chain { # handle 1
    type filter hook input priority 0; policy accept;
    tcp dport http accept # handle 3
    tcp dport nfs accept # handle 7
    tcp dport ssh accept # handle 2
    }
}

Listing Rules

In previous examples above we listed the entire rule set. There are many other ways to list a subset of rules.

List all rules in a given table.

# nft list table inet my_table
table inet my_table {
    chain my_filter_chain {
        type filter hook input priority 0; policy accept;
        tcp dport http accept
        tcp dport nfs accept
        tcp dport ssh accept
    }
}

List all rules in a given chain.

# nft list chain inet my_table my_other_chain
table inet my_table {
    chain my_other_chain {
        udp dport 12345 log prefix "UDP-12345"
    }
}

Sets

nftables has native support for sets. This can be useful if you want a rule to match multiple IP addresses, port numbers, interfaces, or any other match criteria.

Anonymous Sets

Any rule may contain inline sets. This is useful for sets that you don’t expect to change.

The following allows all traffic from 10.10.10.123 and 10.10.10.231.

# nft add rule inet my_table my_filter_chain ip saddr { 10.10.10.123, 10.10.10.231 } accept
# nft list ruleset
table inet my_table {
    chain my_filter_chain {
        type filter hook input priority 0; policy accept;
        tcp dport http accept
        tcp dport nfs accept
        tcp dport ssh accept
        ip saddr { 10.10.10.123, 10.10.10.231 } accept
    }
}

The downside to this method is if you need to alter the set you’ll need to replace the rule. For mutable sets you should use a named set.

As another example, instead of our first three rules we could have used an anonymous set.

# nft add rule inet my_table my_filter_chain tcp dport { http, nfs, ssh } accept

Note: iptables users may be accustomed to using ipset. Since nftables has native set support the use of ipset is not necessary.

Named Sets

nftables also has support for mutable named sets. To create them you must specify the type of elements they will contain. Some example types are; ipv4_addr, inet_service, ether_addr.

Let’s create an empty set to start.

# nft add set inet my_table my_set { type ipv4_addr \; }
# nft list sets
table inet my_table {
    set my_set {
    type ipv4_addr
    }
}

To reference the set in a rule use the @ symbol followed by the set name. The following rule serves as a blacklist for IP addresses added to our set.

# nft insert rule inet my_table my_filter_chain ip saddr @my_set drop
# nft list chain inet my_table my_filter_chain
table inet my_table {
    chain my_filter_chain {
    type filter hook input priority 0; policy accept;
    ip saddr @my_set drop
    tcp dport http accept
    tcp dport nfs accept
    tcp dport ssh accept
    ip saddr { 10.10.10.123, 10.10.10.231 } accept
    }
}

Of course, that’s not very effective since our set is empty. Let’s add some elements.

# nft add element inet my_table my_set { 10.10.10.22, 10.10.10.33 }
# nft list set inet my_table my_set
table inet my_table {
    set my_set {
    type ipv4_addr
    elements = { 10.10.10.22, 10.10.10.33 }
    }
}

However, attempting to add a range value will yield an error.

# nft add element inet my_table my_set { 10.20.20.0-10.20.20.255 }
Error: Set member cannot be range, missing interval flag on declaration
add element inet my_table my_set { 10.20.20.0-10.20.20.255 }

To use ranges in our set we must create the set using the interval flags. This is because the kernel must know in advance what type of data the set will store in order to use the appropriate data structure.

Set Intervals

Sets can also use range values. The is very useful for IP addresses. To use ranges the set must be created with the interval flags.

# nft add set inet my_table my_range_set { type ipv4_addr \; flags interval \; }
# nft add element inet my_table my_range_set  { 10.20.20.0/24 }
# nft list set inet my_table my_range_set
table inet my_table {
    set my_range_set {
    type ipv4_addr
    flags interval
    elements = { 10.20.20.0/24 }
    }
}

Note: The netmask notation was implicitly converted into a range of IP addresses. We could have also used 10.20.20.0-10.20.20.255 to achieve the same effect.

Set Concatenations

Sets also support aggregate types and matches. This means a set element can contain multiple types and a rule can use the concatenation operator . when referencing the set.

This example will allow us to match IPv4 addresses, IP protocols, and port numbers all at once.

# nft add set inet my_table my_concat_set  { type ipv4_addr . inet_proto . inet_service \; }
# nft list set inet my_table my_concat_set
table inet my_table {
    set my_concat_set {
    type ipv4_addr . inet_proto . inet_service
    }
}

Now we can add elements to the list.

# nft add element inet my_table my_concat_set { 10.30.30.30 . tcp . telnet }

As you can see, symbolic names (tcp, telnet) are also usable when adding set elements.

Using the set in a rule is similar to the name set above, but the rule must perform the concatenation.

# nft add rule inet my_table my_filter_chain ip saddr . meta l4proto . tcp dport @my_concat_set accept
# nft list chain inet my_table my_filter_chain
table inet my_table {
    chain my_filter_chain {
    ...
    ip saddr { 10.10.10.123, 10.10.10.231 } accept
    meta nfproto ipv4 ip saddr . meta l4proto . tcp dport @my_concat_set accept
    }
}

Also worth noting is that concatenation can be used with inline sets. Here is one last example showing that.

# nft add rule inet my_table my_filter_chain ip saddr . meta l4proto . udp dport { 10.30.30.30 . udp . bootps } accept

Hopefully you now understand how powerful nftables sets are.

Note: nftables set concatenations are similar to ipset’s aggregate types, e.g. hash:ip,port.

Verdict Maps

Verdict maps are a very interesting feature in nftables that allow you to perform an action based on packet information. Said more plainly, they map match criteria to an action.

Say for example, in order to logically divide your ruleset you want dedicated chains for processing TCP and UDP packets. You can use a verdict map to steer packets to those chains using a single rule.

# nft add chain inet my_table my_tcp_chain
# nft add chain inet my_table my_udp_chain
# nft add rule inet my_table my_filter_chain meta l4proto vmap { tcp : jump my_tcp_chain, udp : jump my_udp_chain }
# nft list chain inet my_table my_filter_chain
table inet my_table {
    chain my_filter_chain {
    ...
    meta nfproto ipv4 ip saddr . meta l4proto . udp dport { 10.30.30.30 . udp . bootps } accept
    meta l4proto vmap { tcp : jump my_tcp_chain, udp : jump my_udp_chain }
    }
}

Of course, just like sets you can create mutable verdict maps.

# nft add map inet my_table my_vmap { type inet_proto : verdict \; }

Your eyes don’t deceive you. The syntax is very similar to sets. In fact, internally sets and verdict maps are built using a common data type.

Now you can use the mutable verdict map in a rule.

# nft add rule inet my_table my_filter_chain meta l4proto vmap @my_vmap

Tables Are Namespaces

One interesting thing about tables in nftables is that they’re also full namespaces. This means that two tables can create chains, sets, and other objects that have the same name.

# nft add table inet table_one
# nft add chain inet table_one my_chain
# nft add table inet table_two
# nft add chain inet table_two my_chain
# nft list ruleset
...
table inet table_one {
    chain my_chain {
    }
}
table inet table_two {
    chain my_chain {
    }
}

This property means applications can organize rules into their own table without impacting other applications. In iptables it was very difficult for applications to make firewall changes without impacting other applications.

However, there is a caveat to this. Each table and chain hook can be viewed as an independent and separate firewall. This means a packet must be accepted by all of them in order to be allowed. If table_one accepts a packet, it may still be dropped by table_two. This is where hook priorities come into play. A chain with a lower priority value is guaranteed to be executed before a chain with a higher priority value. If the priorities are equal, then the behavior is undefined.

Save and Restore a Ruleset

nftables rules can easily be saved and restored. The list output of nft can be fed back into the tool to restore everything. This is exactly how the nftables systemd service works.

To save your ruleset

# nft list ruleset > /root/nftables.conf

To restore your ruleset

# nft -f /root/nftables.conf

Of course, you can enable the systemd service and have your rules restored on reboot. The service reads rules from /etc/sysconfig/nftables.conf.

# systemctl enable nftables
# nft list ruleset > /etc/sysconfig/nftables.conf

Note: Some distributions, RHEL-8 included, ship predefined nftables configuration in /etc/nftables. These samples often include the setup of tables and chains in a manner similar to iptables. These are often listed in the existing /etc/sysconfig/nftables.conf file, but may be commented out.

Conclusions

Hopefully this post served as a solid introduction and demonstration of nftables capabilities. We only scratched the surface of nftables. There are many features not discussed here. You can find more information in the nft manual page and the upstream wiki. Additionally look for some follow up posts on this blog in which we’ll dig into some advanced nftables topics.