nftables – a more universal type of firewall system

July 22, 2021

Now, let’s turn our attention to nftables, the new kid on the block. So, what does nftables bring to the table? (Yes, the pun was intended.):

  • You can forget about needing separate daemons and utilities for all of the different networking components. The functionality of iptables, ip6tables, ebtables, and arptables is now all combined in one neat package. The nft utility is now the only firewall utility that you’ll need.
  • With nftables, you can create multi-dimensional trees to display your rulesets. This makes troubleshooting vastly easier because it’s now easier to trace a packet all the way through all of the rules.
  • With iptables, you have the filter, NAT, mangle, and security tables installed by default, whether or not you use each one.
  • With nftables, you only create the tables that you intend to use, resulting in enhanced performance.
  • Unlike iptables, you can specify multiple actions in one rule, instead of having to create multiple rules for each action.
  • Unlike iptables, new rules get added atomically. (That’s a fancy way of saying that there’s no longer a need to reload the entire ruleset in order to just add one rule.)
  • nftables has its own built-in scripting engine, allowing you to write scripts that are more efficient and more human-readable.
  • If you already have lots of iptables scripts that you still need to use, you can install a set of utilities that will help you convert them into nftables format.

The only real downside to nftables is that it’s still very much a work in progress. It will do most of what you can do with iptables, but the versions of nftables that come with Ubuntu versions 18.04 through 19.04 still lack some of the more advanced features that you might take for granted with the iptables-based solutions. Having said that, the latest version of nftables has somewhat fixed that problem, but at the time of writing, it’s not in the current versions of Ubuntu. (That might change by the time you read this.) To see which version of nftables you have, use the following command:

sudo nft -v

If the version shows up as version 0.9.1 or later, then you have more features available to you than you had previously. 

Learning about nftables tables and chains

If you’re used to iptables, you might recognize some of the nftables terminology. The only problem is that some of the terms are used in different ways, with different meanings. Let’s go over some examples so that you know what I’m talking about:

  • Tables: Tables in nftables refer to a particular protocol family. The table types are ip, ip6, inet, arp, bridge, and netdev.
  • Chains: Chains in nftables roughly equate to tables in iptables. For example, in nftables, you could have filter, route, or NAT chains.

Getting started with nftables

Let’s start with a clean snapshot of our Ubuntu virtual machine, with ufw disabled, and install the nftables package like so:

sudo apt install nftables

Now, let’s take a look at the list of installed tables:

sudo nft list tables
You didn’t see any tables, did you? So, let’s load some up.

Configuring nftables on Ubuntu 16.04

If you ever need to work with the older Ubuntu 16.04, you’ll see that the default nftables.conf file in the /etc directory already has the beginnings of a basic nft firewall configuration:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0;
iif lo accept
ct state established,related accept
# tcp dport { 22, 80, 443 } ct state new accept
ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept

counter drop
}
}

We’ll look at how to work with this in just a bit.

Configuring nftables on Ubuntu 18.04

On the Ubuntu 18.04 virtual machines that we’ll be using, the default nftables.conf file is nothing more than a meaningless placeholder. The file you need, which you’ll copy over to replace the default nftables.conf file, is elsewhere. Let’s check it out. 

First, we’ll go into the directory where the sample configurations are stored and list the sample configuration files:

cd /usr/share/doc/nftables/examples/syntax
ls -l

You should see something similar to the following:

donnie@munin:/usr/share/doc/nftables/examples/syntax$ ls -l
total 60
-rw-r--r-- 1 root root 150 Feb 2 2018 arp-filter
-rw-r--r-- 1 root root 218 Feb 2 2018 bridge-filter
-rw-r--r-- 1 root root 208 Feb 2 2018 inet-filter
. . .
. . .
-rw-r--r-- 1 root root 475 Feb 2 2018 README
-rw-r--r-- 1 root root 577 Feb 2 2018 workstation
donnie@munin:/usr/share/doc/nftables/examples/syntax

If you view the contents of the workstation file, you’ll see that it’s the same as the old default nftables.conf file on Ubuntu 16.04.

Next, we’ll copy the workstation file over to the /etc directory, changing its name to nftables.conf (note that this will overwrite the old nftables.conf file, which is what we want):

sudo cp workstation /etc/nftables.conf

For either Ubuntu 16.04 or Ubuntu 18.04, here’s the breakdown of what you’ll see in the /etc/nftables.conf file that you’ll be using:

  • #!/usr/sbin/nft -f: Although you can create normal Bash shell scripts with nftables commands, it’s better to use the built-in scripting engine that’s included with nftables. That way, we can make our scripts more human-readable, and we don’t have to type nft in front of everything we want to execute.
  • flush ruleset: We want to start with a clean slate, so we’ll flush out any rules that may have already been loaded.
  • table inet filter: This creates an inet family filter, which works for both IPv4 and IPv6. The name of this table is filter, but it could just as well have been something a bit more descriptive.
  • chain input: Within the first pair of curly brackets, we have a chain called input. (Again, the name could have been something more descriptive.)
  • type filter hook input priority 0;: Within the next pair of curly brackets, we define our chain and list the rules. This chain is defined as a filter type. hook input indicates that this chain is meant to process incoming packets. Because this chain has both a hook and a priority, it will accept packets directly from the network stack.
  • Finally, we have the standard rules for a very basic host firewall, starting with the Input Interface (iif) rule, which allows the loopback interface to accept packets.
  • Next is the standard connection tracking (ct) rule, which accepts traffic that’s in response to a connection request from this host.
  • Then, there’s a commented-out rule to accept Secure Shell and both secure and nonsecure web traffic. ct state new indicates that the firewall will allow other hosts to initiate connections to our server on these ports.
  • The ipv6 rule accepts neighbor discovery packets, allowing IPv6 functionality.
  • The counter drop rule at the end silently blocks all other traffic and counts both the number of packets and the number of bytes that it blocks. (This is an example of how you can have one nftables rule perform multiple different actions.)

If all you need on your Ubuntu server is a basic, no-frills firewall, your best bet is to just edit this /etc/nftables.conf file so that it suits your own needs. For starters, let’s set this up to match the setup that we created for the iptables section. In other words, let’s say that this is a DNS server, and we need to allow connections to port 22 and port 53. Remove the comment symbol from in front of the tcp dport line, get rid of ports 80 and 443, and add port 53. The line should now look like this:

tcp dport { 22, 53 } ct state new accept

Note how you can use one nftables rule to open multiple ports.

DNS also uses port 53/udp, so let’s add a line for it:

udp dport 53 ct state new accept

When you’re only opening one port, you don’t need to enclose that port number within curly brackets. When opening multiple ports, just include the comma-separated list within curly brackets, with a blank space after each comma, before the first element, and after the last element.

Load the configuration file and view the results:

donnie@ubuntu-nftables:/etc$ sudo systemctl reload nftables
donnie@ubuntu-nftables:/etc$ sudo nft list ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
iif "lo" accept
ct state established,related accept
tcp dport { ssh, domain } ct state new accept
udp dport domain ct state new accept
icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
counter packets 1 bytes 32 drop
}
}
donnie@ubuntu-nftables:/etc$

The counter drop rule is another example of how an nftables rule can do multiple things. In this case, the rule drops and counts unwanted packets. So far, the rule has blocked one packet and 32 bytes. To demonstrate how this works, let’s say that we want to make a log entry when packets are dropped. Just add the log keyword to the drop rule, like so:

counter log drop

To make these messages easier to find, add a tag to each log message, like this:

counter log prefix "Dropped packet: " drop

Now, when you need to peruse the /var/log/kern.log file to see how many dropped packets you’ve had, just search for the Dropped packet text string.

Now, let’s say that we want to block certain IP addresses from reaching the Secure Shell port of this machine. Do so this, we can edit the file, placing a drop rule above the rule that opens port 22. The relevant section of the file would look like this:

tcp dport 22 ip saddr { 192.168.0.7, 192.168.0.10 } log prefix "Blocked SSH packets: " drop

tcp dport { 22, 53 } ct state new accept

After we reload the file, we’ll be blocking SSH access from two different IPv4 addresses. Any attempts to log in from either of those two addresses will create a /var/log/kern.log message with the Blocked SSH packets tag. Note that we’ve placed the drop rule ahead of the accept rule because if the accept rule gets read first, the drop rule won’t have an effect.

Next, we need to allow the desired types of ICMP packets, like so:

ct state new,related,established icmp type { destination-unreachable, time-exceeded, parameter-problem } accept

ct state established,related,new icmpv6 type { destination-unreachable, time-exceeded, parameter-problem } accept

In this case, you need separate rules for ICMPv4 and ICMPv6.

Finally, we’ll block invalid packets by adding a new prerouting chain to the filter table, like so:

chain prerouting {
type filter hook prerouting priority 0;

ct state invalid counter log prefix "Invalid Packets: " drop

tcp flags & (fin|syn|rst|ack) != syn ct state new counter log drop
}

Now, we can save the file and close the text editor.

Note:

Due to formatting constraints, I can’t show the entire completed file here. To see the whole file, download the code file from the Packt website, and look in the Chapter 4 directory. The example file you seek is the nftables_example_1.conf file.

Now, let’s load up the new rules:

sudo systemctl reload nftables

Another really cool thing to note is how we’ve mixed IPv4 (ip) rules with IPv6 (ip6) rules in the same configuration file. Also, unless we specify otherwise, all the rules that we create will apply to both IPv4 and IPv6. That’s the beauty of using an inet-type table. For simplicity and flexibility, you’ll want to use inet tables as much as possible, rather than separate tables for IPv4 and IPv6.

Most of the time, when all you need is just a simple host firewall, your best bet would be to just use this nftables.conf file as your starting point, and edit the file to suit your own needs. However, there’s also a command-line component that you may find useful.

Using nft commands

My preferred method of working with nftables is to just start with a template and hand-edit it to my liking, as we did in the previous section. But for those who’d rather do everything from the command line, there’s the nft utility.

Note:

Even if you know that you’ll always create firewalls by hand-editing nftables.conf, there are still a couple of practical reasons to know about the nft utility. 

Let’s say that you’ve observed an attack in progress, and you need to stop it quickly without bringing down the system. With an nft command, you can create a custom rule on the fly that will block the attack. Creating nftables rules on the fly also allows you to test the firewall as you configure it, before making any permanent changes.

And if you decide to take a Linux security certification exam, you might see questions about it. (I happen to know.)

There are two ways to use the nft utility. For one, you could just do everything directly from the Bash shell, prefacing every action you want to perform with nft, followed by the nft subcommands. The other way is to use nft in interactive mode. For our present purposes, we’ll just go with the Bash shell.

First, let’s delete our previous configuration and create an inet table since we want something that works for both IPv4 and IPv6. We’ll want to give it a somewhat descriptive name, so let’s call it ubuntu_filter:

sudo nft delete table inet filter
sudo nft list tables
sudo nft add table inet ubuntu_filter
sudo nft list tables

Next, we’ll add an input filter chain to the table that we just created (note that since we’re doing this from the Bash shell, we need to escape the semicolon with a backslash):

sudo nft add chain inet ubuntu_filter input { type filter hook input priority 0\; policy drop\; }

We could have given it a more descriptive name, but for now, input works. Within the pair of curly brackets, we’re setting the parameters for this chain.

Each nftables protocol family has its own set of hooks, which define how packets will be processed. For now, we’re only concerned with the ip/ip6/inet families, which have the following hooks:

  • Prerouting
  • Input
  • Forward
  • Output
  • Postrouting

Of these, we’re only concerned with the input and output hooks, which apply to filter-type chains. By specifying a hook and a priority for our input chain, we’re saying that we want this chain to be a base chain that will accept packets directly from the network stack. You will also see that certain parameters must be terminated by a semicolon, which in turn would need to be escaped with a backslash if you’re running the commands from the Bash shell. Finally, we’re specifying a default policy of drop. If we had not specified drop as the default policy, then the policy would have been accept by default.

Note:

Every nft command that you enter takes effect immediately. So, if you’re doing this remotely, you’ll drop your Secure Shell connection as soon as you create a filter chain with a default drop policy.

Some people like to create chains with a default accept policy, and then add a drop rule as the final rule. Other people like to create chains with a default drop policy, and then leave off the drop rule at the end. Be sure to check your local procedures to see what your organization prefers.

Verify that the chain has been added. You should see something like this:

donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter
[sudo] password for donnie:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
}
}
donnie@ubuntu2:~$

That’s great, but we still need some rules. Let’s start with a connection tracking rule and a rule to open the Secure Shell port. Then, we’ll verify that they were added:

sudo nft add rule inet ubuntu_filter input ct state established accept
sudo nft add rule inet ubuntu_filter input tcp dport 22 ct state new accept

donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter
table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established accept
tcp dport ssh ct state new accept
}
}
donnie@ubuntu2:~

Okay, that looks good. You now have a basic, working firewall that allows Secure Shell connections. We forgot to create a rule to allow the loopback adapter to accept packets. Since we want this rule to be at the top of the rules list, we’ll use insert instead of add:

sudo nft insert rule inet ubuntu_filter input iif lo accept

donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter
table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established accept
tcp dport ssh ct state new accept
}
}
donnie@ubuntu2:~$

Now, we’re all set. But what if we want to insert a rule at a specific location? For that, you’ll need to use list with the -a option to see the rule handles:

donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter -a
table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept # handle 4
ct state established accept # handle 2
tcp dport ssh ct state new accept # handle 3
}
}
donnie@ubuntu2:~$

As you can see, there’s no real rhyme or reason to the way the handles are numbered. Let’s say that we want to insert the rule about blocking certain IP addresses from accessing the Secure Shell port. We can see that the SSH accept rule is handle 3, so we’ll need to insert our drop rule before it. This command will look like this:

sudo nft insert rule inet ubuntu_filter input position 3 tcp dport 22 ip saddr { 192.168.0.7, 192.168.0.10 } drop

donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter -a
table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept # handle 4
ct state established accept # handle 2
tcp dport ssh ip saddr { 192.168.0.10, 192.168.0.7} drop # handle 6
tcp dport ssh ct state new accept # handle 3
}
}
donnie@ubuntu2:~$

So, to place the rule before the rule with the handle 3 label, we have to insert it at position 3. The new rule that we just inserted has the label handle 6. To delete a rule, we have to specify the rule’s handle number:

sudo nft delete rule inet ubuntu_filter input handle 6

donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter -a
table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept # handle 4
ct state established accept # handle 2
tcp dport ssh ct state new accept # handle 3
}
}
donnie@ubuntu2:~$

As is the case with iptables, everything you do from the command line will disappear once you reboot the machine. To make it permanent, let’s redirect the output of the list subcommand to the nftables.conf configuration file (of course, we’ll want to have made a backup copy of the already-existing file, in case we want to revert back to it):

sudo sh -c "nft list table inet ubuntu_filter > /etc/nftables.conf"

Due to a quirk in the Bash shell, we can’t just redirect output to a file in the /etc directory in the normal manner, even when we use sudo. That’s why I had to add the sh -c command, with the nft list command surrounded by double quotes. Also, note that the file has to be named nftables.conf because that’s what the nftables systemd service looks for. Now, when we look at the file, we’ll see that there are a couple of things that are missing:

table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established accept
tcp dport ssh ct state new accept
}
}

Those of you who are sharp-eyed will see that we’re missing the flush rule and the shebang line to specify the shell that we want to interpret this script. Let’s add them:

#!/usr/sbin/nft -f
flush ruleset
table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established accept
tcp dport ssh ct state new accept
}
}

Much better. Let’s test this by loading the new configuration and observing the list output:

sudo systemctl reload nftables

donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter
table inet ubuntu_filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established accept
tcp dport ssh ct state new accept
}
}
donnie@ubuntu2:~$

That’s all there is to creating your own simple host firewall. Of course, running commands from the command line, rather than just creating a script file in your text editor, does make for a lot more typing. However, it does allow you to test your rules on the fly, as you create them. And creating your configuration in this manner and then redirecting the list output to your new configuration file relieves you of the burden of having to keep track of all of those curly brackets as you try to hand-edit the file.

It’s also possible to take all of the nft commands that we just created and place them into a regular, old-fashioned Bash shell script. Trust me, though, you really don’t want to do that. Just use the nft-native scripting format, as we’ve done here, and you’ll have a script that performs better and is much more human-readable.

Hands-on lab for nftables on Ubuntu

For this lab, you’ll need a clean snapshot of your Ubuntu 18.04 virtual machine:

  • Restore your Ubuntu virtual machine to a clean snapshot to clear out any firewall configurations that you created previously. (Or, if you prefer, start with a new virtual machine.) Disable ufw and verify that no firewall rules are present:
sudo systemctl disable --now ufw
sudo iptables -L

You should see no rules listed for iptables.

  • Install the nftables package:
sudo apt install nftables
  • Copy the workstation template over to the /etc directory and rename it nftables.conf:
sudo cp /usr/share/doc/nftables/examples/syntax/workstation /etc/nftables.conf
  • Edit the /etc/nftables.conf file to create your new configuration. (Note that due to formatting constraints, I have to break this into three different code blocks.) Make the top portion of the file look like this:
#!/usr/sbin/nft -f flush ruleset
table inet filter {
chain prerouting {
type filter hook prerouting priority 0;
ct state invalid counter log prefix "Invalid Packets: " drop

tcp flags & (fin|syn|rst|ack) != syn ct state new counter log prefix "Invalid Packets 2: " drop
}
  • Make the second portion of the file look like this:
chain input {
type filter hook input priority 0;
# accept any localhost traffic
iif lo accept
# accept traffic originated from us
ct state established,related accept
# activate the following line to accept common local services
tcp dport 22 ip saddr { 192.168.0.7, 192.168.0.10 } log prefix "Blocked SSH packets: " drop

tcp dport { 22, 53 } ct state new accept
udp dport 53 ct state new accept
ct state new,related,established icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
  • Make the final portion of the file look like this:
ct state new,related,established icmpv6 type { destination-unreachable, time-exceeded, parameter-problem } accept

# accept neighbour discovery otherwise IPv6 connectivity breaks.
ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept

# count and drop any other traffic
counter log prefix "Dropped packet: " drop
}
}
  • Save the file and reload nftables:
sudo systemctl reload nftables
  • View the results:
sudo nft list tables
sudo nft list tables
sudo nft list table inet filter
sudo nft list ruleset
  • From either your host computer or from another virtual machine, do a Windows scan against the Ubuntu virtual machine: 
sudo nmap -sW ip_address_of_UbuntuVM
  • Look at the packet counters to see which blocking rule was triggered (hint: it’s in the prerouting chain):
sudo nft list ruleset
  • This time, do a null scan of the virtual machine:
sudo nmap -sN ip_address_of_UbuntuVM
  • Finally, look at which rule was triggered this time (hint: it’s the other one in the prerouting chain):
sudo nft list ruleset
  • In the /var/log/kern.log file, search for the Invalid Packets text string to view the messages about the dropped invalid packets.

That’s the end of this lab – congratulations!

In this section, we looked at the ins and outs of nftables, and looked at ways to configure it to help prevent certain types of attacks. Next, we’ll turn our attention to the mysteries of firewalld.

Related Articles

How to add swap space on Ubuntu 21.04 Operating System

How to add swap space on Ubuntu 21.04 Operating System

The swap space is a unique space on the disk that is used by the system when Physical RAM is full. When a Linux machine runout the RAM it use swap space to move inactive pages from RAM. Swap space can be created into Linux system in two ways, one we can create a...

read more

Lorem ipsum dolor sit amet consectetur

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

19 − eighteen =