Nftables Part 2
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}
define portmap = 111
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
tcp dport $portmap drop
udp dport $portmap drop
}
chain output {
type filter hook output priority 0; policy accept;
tcp dport $portmap drop
udp dport $portmap drop
}
}
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 are two definitions 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. Being such an important service though it does attract a lot of attention from hackers attempting dictionary attack, or it may also have been tested by port scanners. Neither of which do I want approaching my VPS I have therefore set up a system using a named set and dynamic editing of the set to block them for a while. I will introduce and describe this later in the article.
define portmap = 111 An embarrassing legacy item this. Some years ago, I received a notification from Bitfolk that my VPS was running a very insecure open service on port 111. Not only was it insecure but totally unnecessary. I removed the service and blocked the port, the block has remained as a legacy item in every firewall since. This definition and the rules referencing it are probably not needed any more.
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 types 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 a new one 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 default 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 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 is 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
The next two rules make use of connection tracking. The first checks the state against the members of an unnamed set of values 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
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
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
https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes provides a good cheat sheet when writing new rules