Introduction

I wanted to write a short article about my experience how I switched to the new NFT firewall – a.k.a. nftables – on Linux from the old, legacy implementation which I had used for more than a decade. What brought the new firewall up and got my attention about NFT is when Debian OS Buster release switched over to it. Honestly, I was quite satisfied with the legacy implementation before and could not think of anything good what NFT could bring on the table for an experienced system engineer as was me (with 20y of having a Linux hat). However, when I had looked into it and tried it, soon I got to like it! 🙂 So without hesitation, I decided to migrate my previously existing Linux firewall script. Here is how I found out that I had used the old, legacy implementation of the firewall.

root@localhost:~# iptables --version
iptables v1.8.2 (legacy)
root@localhost:~#

In the above command line sample, if you see something like legacy, then you are using the legacy implementation. If you see something like nf_tables, then you are using the new implementation.

Why I like it

The old, legacy implementation is not that bad. It is usable. However, the NFT is much better and clearer to view and work with! 🙂 First it takes some time to understand the idea, but after a while reading the docs, man page and the Wiki and of course practicing it, it easy to learn it. Overall besides having a cleaner visibility of rules, the thing I like the most about NFT is the ability to mix it with IPv4 and IPv6 addresses. You no longer have to separate the different type of IP addresses. And that is exactly why I switched over to it. At my home Internet service provider, I am served freely with both IPv4 and IPv6 addresses thankfully, so I can experiment with it. The other thing which is minor, to be able to name your input/output rulesets as you wish. You do not need to stick with uppercase or lowercase letters. Let me show you how I open the outward traffic to WHOIS servers (as a rule of thumb, I always restrict inward as well as outward traffic and only permit what I want) as part of my allowed outgoing traffic. This is part of the OUTPUT chain called ‘inet filter’.

root@qmiacer:~# nft list chain inet filter OUTPUT
table inet filter {
chain OUTPUT {

(...)

Permitting WHOIS protocol queries outward to public servers

As you can see from the above, the same ruleset contains lines for IPv6 and IPv4 addresses. The first 3 lines permit traffic to destination IPv6 addresses TCP port 43 with counting the number of packets (that is what counter keyword is for). The 4th line permits outgoing traffic to an IPv4 address (193.0.6.135) TCP port 43. All in one block. And that’s the beauty in it :-). Let’s look at another example in the incoming rule chain, called as INPUT ‘inet filter’.

root@qmiacer:~# nft list chain inet filter INPUT
table inet filter {
chain INPUT {

(...)

Permitting inward ICMP traffic

As you can see, I start by disabling all traffic and only permit what is necessary. This is a basic policy for me and it is due to security reasons. Then the line below means to accept all traffic from the loopback (lo) interface (used for local traffic). Next line is to permit all TCP packets with established and related states. Those are the ones already allowed for connection and known to my host. The following 3 lines are examples of ICMPv4 packets with specific types. The first two is from source addresses of reserved IP address space 192.168.0.0/16 in CIDR notation. The third line is to permit ICMPv6 packets with types as required by the ICMPv6 protocol auto-negotiation. Again, the beauty is that all of them they are listed in one block, I do not to separate them as I used to in the old, legacy implementation! 🙂

Building the ruleset

In fact, to build the ruleset you need to specify the language the NFT binary understands. Firstly, use the following shebang line for your script. This must be the first line of your script. BTW, the nft binary is part of the nftables package on Linux Mint 20.

#!/usr/sbin/nft -f

Then you can follow along. For e.g., the following line clears all rules. It is going to empty your firewall enabling every traffic.

flush ruleset

That’s it. One line clears it all. Then start building your rules for different goals. One for inbound (INPUT), one for routing (FORWARD) and one for outbound traffic (OUTPUT). Although routing is not applicable in most cases unless you would like to allow packets routing through your host. The following will build your empty rulesets for holding the rules in each section later on.

add table inet filter
add chain inet filter INPUT { type filter hook input priority filter; policy drop; }
add chain inet filter FORWARD { type filter hook forward priority filter; policy drop; }
add chain inet filter OUTPUT { type filter hook output priority filter; policy drop; }

Note that the above will disable ALL TRAFFIC by default. So do not turn your firewall on yet. The following task is to build your own rules and add them into each ruleset section to permit what you want. So for example, permitting all traffic for localhost (lo) , you would use the following line:

add rule inet filter INPUT iifname "lo" accept

Permitting the established, related type of TCP packets would go as the following line on its own:

add rule inet filter INPUT ct state related,established accept

And so on.

Deleting (removing) rules from your firewall

Here I am going to show you how to delete rules if you accidentally added them or just want to remove them. First list them with so called ‘handles’. Each rules has a handle ID.

# nft list chain inet filter OUTPUT -a

You can see a sample output of the nft list command. The ‘-a’ switch tells to list the handle IDs. Then let’s say you would like to remove line with handle 56. You will do:

# nft delete rule inet filter OUTPUT handle 56

That’s it. The above command will delete the rule from firewall with handle ID 56.

Blocking an individual IP address

What if you want to block an evil IP address to ban traffic in case of an DoS attack? You need to create a set and add element to the set. Each element will be an evil IP address. To create an element:

add set inet filter blackhole { type ipv4_addr; flags timeout; size 65536; }

To add an element to the set (add multiple IP addresses if you have):

add element inet filter blackhole { x.yyy.zzz.www }

Then in the end, you have to drop the traffic that is in the set by adding the blackhole set to the INPUT block:

add rule inet filter INPUT ip saddr @blackhole counter drop

Then you should be good to go. The evil IP address will be blocked . In my case, I have set the ‘inet filter INPUT‘ , but that can be customized, no capital letter required either.

Summary 

If you have any questions or would like to hire me to help you in setting up your Linux firewall, you are welcome to do so! Reach me out on Discord, my ID is qmi#7610 . Or send me an e-mail message at info@foresthacker.hu .

qmi, 15 December 2020