Nftables simplifies dual stack handling and atomic rule updates compared to iptables which will replace all rules even if only one rule needs to be replaced. We will be using a table of address family inet which will allow for hybrid ip and ip6 addresses in the same table. While chains act as containers for rules, tables can be used to aggregate chains, sets and maps.
Variables can be declared using the define statement where multiple elements need to be wrapped in a set using parantheses. They can then be referenced as $variable
.
Rules may contain multiple actions per rule and can be updated dynamically without altering the rest of the ruleset.
#!/usr/sbin/nft -f flush ruleset define ns1_v4 = { 172.16.0.1, 10.0.0.1 } define ns1_v6 = 2a02:beef:fa:1000::1 table inet filter { set tcp_accept { type inet_service flags interval elements = { ssh, smtp, domain, http, imap2, https, submission, sieve } } set udp_accept { type inet_service flags interval elements = { domain } } chain global { iif "lo" accept ct state established,related accept ct state invalid counter drop ip protocol icmp accept ip6 nexthdr ipv6-icmp accept } chain input { type filter hook input priority 0; policy drop; tcp dport { ssh } ip saddr @f2b-sshd drop jump global tcp dport @tcp_accept accept udp dport @udp_accept accept counter drop } ... }
If a packet gets accepted/dropped and there is a later chain in the same hook which is ordered with a later priority, the packet will be evaluated again. This is, the packet will traverse all the chains in a given hook.
Base chains, chains that use netfilter hooks, allow filtering packets selectively without the need for jumps as hooks with higher priority will evaluate our packets again.
chain dns-in { type filter hook input priority 1; udp dport domain ip daddr $ns1_v4 counter tcp dport domain ip daddr $ns1_v4 counter udp dport domain ip6 daddr $ns1_v6 counter tcp dport domain ip6 daddr $ns1_v6 counter }
We will use concatenations to explicitly describe sets and rules. Using this approach we are able to create IP to TCP port correlations which we will use to filter traffic on our base input chain and drop everything else. Since the dport match requires a prefixed protocol selector we will omit this in the set and add specific dport rules in the base chain for readability.
set ipv4_tcp_services { type ipv4_addr . inet_service elements = { $vhosts1_v4 . http, $vhosts1_v4 . https, $ns1_v4 . domain } } ... chain input { type filter hook input priority 0; policy drop; jump global ip daddr . tcp dport @ipv4_tcp_services accept ip daddr . udp dport @ipv4_udp_services accept ip6 daddr . tcp dport @ipv6_tcp_services accept ip6 daddr . udp dport @ipv6_udp_services accept counter drop }
nftables families:
ip
Tables of this family will see IPv4 traffic/packets.ip6
Tables of this family will see IPv6 traffic/packets.inet
Both IPv4/IPv6 packets will traverse the same rules. Rules for IPv4 packets won’t affect IPv6 packets. Rules for both L3 protocol will affect both.arp
Tables of this family will see ARP-level (i.e, L2) traffic, before any L3 handling is done by the kernel.bridge
Tables of this family will see traffic/packets traversing bridges (i.e. switching). No assumptions are made about L3 protocols.netdev
This family provides the ingress hook, that allows you to classify packets that the driver has just passed up to the networking stack. This means you see all network traffic for your NIC getting in. No assumptions are made about L2 and L3 protocols, therefore you can filter ARP traffic from here.
Chains
Chains the containers for our rules can be modified easily.
Let’s add a new chain to our table:
nft add chain inet filter name
List a specific chain:
nft list chain inet filter name
Remove chain:
nft flush chain inet filter name nft delete chain inet filter name
Named sets
We add a set that will concatenate an ip and a destination port as an element:
nft add set inet filter name { type ipv4_addr . inet_service\; } nft add element inet filter name { 172.16.0.1 . http }
The resulting set will look like this.
nft list set inet filter name table inet filter { set name { type ipv4_addr . inet_service elements = { 172.16.0.1 . http } } }
Rules
List all rules with handles so they can be referenced using their individual IDs when modifying them:
nft list ruleset -a
The add statement will append a rule to the end of the chain if no handle statement is specified.
nft add rule inet filter dns-in udp dport domain ip daddr @ns1_v4 counter
Likewise insert will prepend the rule to the start of the chain if no handle is specified.
nft insert rule inet filter dns-in udp dport domain ip daddr @ns1_v4 counter
Replace a specific rule:
nft replace rule inet filter input handle nn tcp dport { http, https} ip6 daddr 2a02:beef:fa:1000::1 counter nft replace rule inet filter input handle nn counter log prefix \"nft/input [DROP]\" group 2 drop
Logging
Nftables uses Ulogd2 to log its activities. We will configure a new logging stack in
stack=log2:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,print1:PRINTPKT,emu2:LOGEMU # packet logging through NFLOG for group 1 [log2] group=1 # Group has to be different from the one use in log1 [emu2] file="/var/log/ulog/nftables.invalid.log" sync=1
We will be using this group with a custom log prefix:
ct state invalid counter log prefix "nft/global [CT INVALID DROP]" group 1 drop
fail2ban
Use a custom configuration
[Definition] actionstart = <nftables> add set <nftables_family> <nftables_table> <set_name> \{ type <nftables_type>\; \} <nftables> insert rule <nftables_family> <nftables_table> <chain> %(nftables_mode)s <address_family> saddr @<set_name> counter log prefix \"nft/input [f2b DROP]\" group 2 <blocktype> _nft_get_handle_id = grep -m1 '<address_family> saddr @<set_name> counter packets.*bytes.*log prefix \"nft/input \[f2b DROP\]" group 2 <blocktype> # handle' | grep -oe ' handle [0-9]*' [Init] blocktype = drop