Introduction

This is a continuation of [NFTables part 1] which just looked at installing and basic concepts. This page looks in greater depth at a ruleset and the syntax of the rules it contains

Dissecting a Ruleset Script

#!/usr/sbin/nft -f
flush ruleset
define web = {http,https}
define mail = {smtp,pop3,imap3,submission}
define altssh = 22
define ftpports = {ftp,ftps}
table inet filter {
    chain input {
            type filter hook input priority 0; policy drop;
            meta iif lo accept
            tcp dport $altssh counter accept
            ct state {established,related} accept
            ct state invalid drop
            tcp dport {$web,$mail,$ftpports,webmin,domain} accept
            udp dport domain accept
            icmp type echo-request limit rate 10/second accept
            ip6 nexthdr icmpv6 limit rate 10/second accept
            ip saddr 192.0.2.10 accept
            ip6 saddr 2001:db8:beef:cafe::/64 counter accept
    }

    chain output {
            type filter hook output priority 0; policy accept;    
    }
}

This is a near complete firewall script for a VPS not operating as a router we will take it apart line by line and look at the syntax. Where as any of the text input methods will need to describe and action for nft and a relative position for the rule or chain to be placed, a descriptive ruleset file will not need that as it is already placed in the diagram.

Definition block

The first 2 lines have been explained already. Next come the variable definitions. Each consists of a variable name = followed by the value(s) assigned. When referenced in a rule, the name has to be prefixed with $. These definitions contain many references to built in constants for readability. NFTables uses its own table of aliases for port numbers. These can be viewed using the command

:~#  nft describe tcp dport

payload expression, datatype inet_service (internet network service) (basetype integer), 16 bits
pre-defined symbolic constants (in decimal):
    tcpmux                                             1
    echo                                               7
    ...

The other feature demonstrated here is the set. There are two types of sets, named and anonymous. Both are enclosed in curly braces and elements are comma separated. Anonymous sets, as illustrated here are static and form an integral part of the rule. They cannot have rules added or deleted (except by removing and re-adding the rule) and only exist within the rule where they are defined. Named sets, however, must be defined before use and may be referenced (‘‘@name’’) in any rule following their definition. They can be added to or have elements deleted dynamically.

NOTE There’s another definition that will need a little further explanation

*''define altssh = 22''* 

This may seem surplus to requirements. Afterall, sshd is already defined internally as port 22. It therefore seems pointless defining another name for it when sshd could be added to the rule allowing access to certain fixed ports anyway. The reason I have done this is that I actually have sshd listening on a different non-standard port. Port 22 therefore remains closed and there is no one who has a legitimate reason to attempt to use it. I will come back to port 22 later to illustrate a temporary blacklisting system I have set up.

Table and Chain definitions

add table inet filter

''table inet filter {''

The initial definition merely needs the family and a name as it only acts as a container for the chains within it. It defines the family of packets which it will handle. There are 5 different table families, ip, ip6, inet (a super family of the two previous), arp and bridge. The default is ip. The name can be anything though there are limits on length. For readability a simple descriptive name is obviously best.

There are two classes of chains that can be used. The base chain and the non-base chain. The difference is that a base chain is defined with a hook (e.g. input) and all packets matching that hook will be passed through the chain. Non-base chains have no hook and and will only see packets sent to them by a jump or go to command in a rule in a base chain. We will add some non-base chains later, but the ruleset, at the moment, contains only base chains. The definition of our first chain is

   chain input {
            type filter hook input priority 0; policy drop;
            (various rules placed here)
            }

A generic base chain definition entered in text form would be

add chain [<family>] <table-name> <chain-name> { type <type> hook <hook> priority <value>; [policy <policy>;] } which in the case of our input chain would read

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

Looking at this in more detail. There are three types of chain. Filter which does just that. Route, to reroute packets and nat. The hook parameter determines what packets will be operated on by the chain. The available hooks are prerouting, input, forward, output, postrouting and ingress which operates on packets before even prerouting, it works only with the family netdev. Our chain is of the filter type operating on incoming packets.

The priority parameter determines the order in which chains are applied if there is more than one chain of that type. The lower the number given here the earlier in the order of the chains it appears. Note that this must be followed by a semicolon “;” The semicolon is used to mark the end of a command. If you are entering the definition from a bash command line the semicolon must be escaped “\;”. The final part of the definition is the policy. The two usual values for this are accept or drop (others are possible) and it describes the action (or verdict statement) to be applied to packets which have transversed the whole chain without matching any rules.

The Rules

A rule in its basic form consists of an expression and a verdict statement to be acted on if the expression evaluates to true. There can be more than one expression and more than one statement. The expressions are evaluated from left to right, if the first is true, then the second is evaluated and so on. Similarly the statements are enacted from left to right. However, note that is the first statement is a terminal one e.g. drop, then the packet is dropped without consideration of further statements. So there can only be one terminal statement and it must appear last. As an example log drop will make a log entry and then drop the packet, drop log will drop the packet and the log statement will be ignored. In this second instance you may get a warning when attempting to load the rule.

The expression to be matched may contain operators e.g. eq , != and so on. The default is eq which tends not to be actually written. So the general structure is <property being examined> <operator> <value expected>

Let’s consider the rules in the input chain

meta iif lo accept

Expression = '’meta iif lo’’ action if true = '’accept’’

Expand that expression into more human understandable language

In the metainformation the incoming interface is equal to lo In other words the result is true if it is traffic generated on this machine.

There are lists available of all acceptable selectors, these are too long to include here and links will be given.

'’iif’’ deserves further explanation. There are 4 terms which can be used to describe the interface. iif and oif refer to the incoming and outgoing interfaces respectively. iifname and oifname do likewise. The difference is that the short forms refer to an index of the interfaces and are faster to lookup whereas the longer forms refer to the actual names of the interfaces and are thus slower. If the box only has say lo and eth0 then the short forms will work properly. If it has a number of interfaces that may be dynamically loaded then the long form has to be used.

The final action performed if the expression evaluates to true is accept which is a terminal action, the packet is allowed through and traverses no more rules

tcp dport $altssh counter accept

’‘$altssh’’ was defined in the definition block, at the moment it evaluates to 22 and so that is substituted in the rule. This can be read as a double expression rule in effect. If the protocol is tcp and if the destination port is 22. However that is not quite the case as dport has to be linked to a protocol. dport refers to the destination port and sport to the source port.

The interesting point about this rule is that it has 2 verdict statements, or operations. counter which counts the packets (and bytes) and then accept. ‘‘iptables’’ gave no choice, everything was counted. ‘‘nftables’’ however, allows you to just add counters to whichever rule you want.

Counters can be read by listing the ruleset or chain.

~# nft list ruleset
table inet filter {
    chain input {
            type filter hook input priority 0; policy drop;
            iif "lo" accept
            tcp dport 22 counter packets 9 bytes 824 accept</syntaxhighlight>

The next two rules make use of connection tracking. The first checks the state to see if the packet is part of an established connection, or related one. If so, then everything will have been checked before so the packet is accepted. The second rule of this pair just drops those invalid ones early. Gets them out of the way.

ct state {established,related} accept
ct state invalid drop

The next rule makes use of an unnamed set of ports defined at the beginning of the file and internally defined constants to open common ports. As always, if examining dports we have to state the protocol, in this case tcp. And the following rule opens port 53 on UDP as that is also used by DNS

tcp dport {$web,$mail,$ftpports,webmin,domain} accept
udp dport domain accept</syntaxhighlight>

If you were entering the rule using text input then you would need a line like

add rule filter input udp dport domain accept

It is possible to limit packets received which can help mitigate certain types of attack. The next two rules allow ping requests on ip and ip6 as well as permitting all the various ip6 icmp messages used to establish the network and interface on start up.

icmp type echo-request limit rate 10/second accept
ip6 nexthdr icmpv6 limit rate 10/second accept</syntaxhighlight>

The final four rules should be understandable and self explanatory now. Two (using dummy addresses here) allowing access to two addresses In my proper table, I have inserted the addresses of my second VPS as the two machines will often communicate via ports which I do not want open to the world. The ip and ip6 protocols are explicitly named at the start of the rules here.

What Next

In [[Nftables Part 3]] we will look at chains in greater depth, multiple chains with different priorities, non-base chains and other aspects including structuring the ruleset for easy editing and explore sets more fully - named sets, maps and dictionaries. All of which will be necessary for setting up fail2ban in the final part

Some Useful Resources##

Nftables Wiki provides a good cheat sheet when writing new rules.