Double-click to edit
Tap to edit

Introduction to nftables

How to configure a simple IPv6-ready webserver firewall

Posted on Sept. 19, 2018

Hello all! This is a followup posting of our table talk. In case you have missed our first introduction of the tables, you can check our previous posting IPtables vs. nftables.

Today we would like to introduce you to nftables and how to use it. In this posting we give you a step-by-step-tutorial so you can try the commands by yourself in your own environment! 


A note: nftables is indeed a powerful and advanced tool, and our post today is a basic introduction, meaning we will not cover all possible configuration and command combinations: some of these topics will be covered later as separate postings. 

 

What is nftables?

Nftables is a package filtering framework in Linux. It was built to replace IPtables, a tool developed years ago, and connected tools. Nft is basically used to route and manipulate network packets, or the traffic in general. It defines what packages are routed to what point under predefined circumstances/conditions. For example you probably do not want people you don’t know connecting to your network. Instead, you probably want their requests to be dropped right away from the beginning. It is also good for setting up a private network, like the network of your tech community, and establishing a proper permissions structure. 

iptables-vs-nftables.jpg

So let us get started. After this, you should be able to create a basic nft configuration.

Requirements: 

  • Modern Linux distribution
  • Some time for trying things out and having fun


Getting started

First, you need to check a few things. Check if the nf_tables kernel module is available and check if nft is installed correctly. You can do so by calling: modinfo nf_tables. If everything works as expected, you get an answer like:

terminal.jpgEverything works fine.

Also make sure you have IPtables disabled, by calling iptables -F.

Basic Syntax

Structure

The structure of nftables is pretty easy: there are tables, chains and rules. The tables consist of chains and the chains themselves consist of rules. nftables syntax is different from IPtables syntax. All commands start with „nft“ and an action (add, delete, list, flush), e. g. nft add. Syntax is as follows:

nft add chain [<family>] <table-name> <chain-name> { type <type> hook <hook> priority <value> \; [policy <policy>] }

Commands passed to nft via commandline are only temporary, rules which are loaded through the config file are permanent. Config files are loaded through:

% nft -F filepath

So let’s start with the tables. 


Tables

You can use the following commands.

  • add: for creating a table
  • delete: for deletion
  • list: for displaying and flush for emptying the table.

Tables have a so called "address family". Address families determine the type of packages which are processed. There are the following address families:

  • ip
  • ip6
  • inet ({ip, ip6})
  • arp
  • bridge
  • netdev

Address family inet combines both IPv4 and IPv6 for simplification. Speaking of simplification: it is possible to define variables to reduce redundancies, which were a big issue in IPtables before.

For example if you want to address one or multiple interfaces, you can easily map them to a single variable name:

define foo_interface_int = eth0
define foo_interface_ext = eth1
define foo_interfaces = { $foo_interface_int, $foo_interface_ext }

When creating a table, the params that are needed are only the address family and the name of the table. If you do not specify an address family, the ip family will be used as a default. Creating a table works like:

% nft add table inet foo


Chains

Let’s get to the creation of chains. As said above, a chain consists of one or multiple rules and they are of a predefined type (base and non-base). Base means that the chain has a hook in the kernel so the traffic is exposed to this chain. It is not exposed to a non-base chain. You also have to define a hook, input or output, meaning incoming or outgoing. You add a new base chain to the table foo with:

 % nft add chain ip foo output { type filter hook output priority 0 \; policy accept\; }

The type can be filter, route or nat. The filter type is used for filtering, like its name indicates, and supported by address families ip, ip6 (inet), arp and bridge. Route is used to reroute packets if for example the packet mark gets modified. And nat which is used to perform Network Address Translation (NAT).

Defining a priority is very important because it defines the order of the chains and if you have several chains in the input hook you can influence which chain is traversed before another.

As we have created our first table and our first chain now, it’s time to fill the chain with rules. 


Rules

A rule is built from one or more expressions, an operator and an action that should be performed. Using multiple expressions in a single rule will result in a nested expression. The expressions will then be traversed from left to right. If the first expression matches, the next expression is evaluated. If the first expression doesn’t match, the other expressions will not be evaluated. This is a rule for the simple task of dropping (the "action" in this case) any packets to the destination ip address 1.2.3.4:

% nft add rule ip foo output ip daddr 1.2.3.4 drop

There are no limits to your creativity. This rule was kind of simple but you can also create complex ones by concatenating expressions.

After you know the basics, it's time to put the basics into use. A cool first use case would be, say, configuring a webserver. Sounds fun? Let's get to it.

Webserver configuration

So what we want to do is configuring a simple webserver, and we want to do it right. Our goal is to make it more secure by dropping unsolicited connections and requests that can potentially harm our infrastructure, while still allowing safe and recognized connections.

We also want to allow ICMP pings, connections to our OpenVPN service and we of course want to be allowed to connect to our server via SSH. For our webserver to function, we need to open ports 80 (for HTTP) and 443 (for HTTPS/SSL), so that others can connect to our server and see the content. And last but not least we want to drop everything else that we do not explicitly allow. That's about it. Quite simple, wasn't it? The configuration should look like this:
 

# Clear everything that might be currently in place
flush ruleset

table ip filter {
  chain incoming {
    # Self explaining (see above tutorial)
    type filter hook input priority 0;

    # Allow already established/related connections
    # It is necessary to allow already established connections, because otherwise connections or requests 
    # that were made up by our server cannot be responded to.  
    ct state established,related accept

    # Drop invalid connections
    ct state invalid drop

    # Loopback interface
    iifname lo accept

    # Allow ICMP ping requests, rate limit to prevent ICMP flood
    ip protocol icmp limit rate 10/second accept
    ip protocol icmp drop

    # Allow SSH connections
    tcp dport ssh limit rate 15/minute accept

    # Allow connections to webserver, open ports: 80 (HTTP) and 443 (HTTPS)
    tcp dport {http, https} accept

    # Allow OpenVPN connections
    udp dport 1194 limit rate 15/minute accept

    # Drop everything else which might be unsolicited
    drop
  }
}

 

You may have noticed that we didn't cover IPv6 with this configuration yet, as it solely mentions ip address family and therefore IPv4. Of course you could simply copy the above rules, change a few things (e. g. ip to ip6) and paste it again for IPv6 coverage. But instead you want to have a single configuration for both address families to avoid code duplication.

Code duplication was a problem of the predecessor of nftables, IPtables, like we have mentioned above and in our previous article of this series.  If you have closely followed the tutorial, you might know that "inet" address family in nftables covers both of them and it is easy to combine everything in a single ruleset:
 

flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0;
    ct state established,related accept
    ct state invalid drop
    iifname lo accept
    ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 10/second accept
    ip6 nexthdr icmpv6 icmpv6 type echo-request counter drop

    # allow other relevant ICMP traffic for IPv6
    ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-reply, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept

    ip protocol icmp icmp type echo-request limit rate 10/second accept
    ip protocol icmp icmp type echo-request counter drop
    tcp dport {http, https} accept
    tcp dport ssh limit rate 15/minute accept
    udp dport 1194 limit rate 15/minute accept
    drop
  }
}

 

Comments were omitted mostly because there is no big change compared to the other ruleset.

Put it into real use on VMs

Hey, you've made it this far - congratulations! Now you're able to configure a basic webserver with nftables. Super cool! Still hungry for some facts about nftables? We have prepared some useful tips and a Cheatsheet below.
 

A bonus: useful tips and Cheatsheet

It is possible to add rules directly to whole networks. For blocking incoming connections from a network, you could for example write:

% nft add rule ip foo input ip daddr 1.2.3.4/24 drop 


You can simply delete all rules in a chain by calling: 

% nft delete rule foo output


Flushing the whole chain (Flush/delete all rules from the chain):

% nft flush chain foo input


Sometimes it can be useful to export the nftables ruleset, so here are the commands for both XML and JSON format:

% nft export xml > ruleset.xml
% nft export json > ruleset.json

 

A closing tip: for using nftables for IPv6 VMs, you can check out IPv6 only hosting options - at ipv6onlyhosting.com, VM prices start by only 2.5 CHF/Month. An easy start for prototyping and testing your next cool project with nftables.