One of the main advantage of nftables over iptables is its native handling of set. They can be used for multiple purpose and thanks to the timeout capabilities it is easy to do some fun things like implementing port knocking in user space.
The idea of this technic is fairly simple, a closed port is dynamically opened if the user send packets in order to a predetermine series of ports.
To implement that on a system, let’s start from ruleset where Input is closed but for a SSH rule using a set of IPv4 address as parameter:
# nft list ruleset table inet Filter { set Client_SSH_IPv4 { type ipv4_addr flags timeout elements = { 192.168.1.1, 192.168.1.254 } } chain Input { type filter hook input priority 0; policy drop; ct state established counter accept tcp dport ssh ip saddr @Client_SSH_IPv4 counter accept iif "lo" accept counter log prefix "Input Dft DROP " drop } }
This gives a control of which IP get access to the SSH server. It contains two fixed IP but there is a timeout flag set allowing the definition of entry with timeout.
To implement a 3 steps port knocking we define two sets with a short timeout. Hitting one port make you enter in the first set then if you are in the first set you can enter the second set by hitting the second port.
To get the first step done we use the fact that nftables can add element to a set in the packet path:
# nft add inet Filter Raw tcp dport 36 set add ip saddr @Step1 drop
Once this is done, second step is a simple derivative of first one:
# nft add inet Filter Raw ip saddr @Step1 tcp dport 15 set add ip saddr @Step2 drop
We add these rules in the prerouting table so kernel won’t do any connection tracking for these packets.
This gets us the following ruleset:
set Step1 { type ipv4_addr timeout 5s } set Step2 { type ipv4_addr timeout 5s } chain Raw { type filter hook prerouting priority -300; policy accept; tcp dport 36 set add ip saddr @Step1 drop ip saddr @Step1 tcp dport 15 set add ip saddr @Step2 drop ip saddr @Step2 tcp dport 42 set add ip saddr timeout 10s @Client_SSH_IPv4 drop }
Poor man testing can be
nmap -p 36 IPKNOCK & sleep 1; nmap -p 15 IPKNOCK & sleep 1; nmap -p 42 IPKNOCK
We run this and our IP is in the Client_SSH_IPv4 set:
# nft list set inet Filter Client_SSH_IPv4 table inet Filter { set Client_SSH_IPv4 { type ipv4_addr flags timeout elements = { 192.168.2.1 expires 8s, 192.168.1.1, 192.168.1.254 } } }
and we can SSH to IPKNOCK 😉
And here is the complete ruleset:
# nft list ruleset table inet Filter { set Client_SSH_IPv4 { type ipv4_addr flags timeout elements = { 192.168.1.1, 192.168.1.254 } } set Step1 { type ipv4_addr timeout 5s } set Step2 { type ipv4_addr timeout 5s } chain Input { type filter hook input priority 0; policy drop; ct state established counter accept tcp dport ssh ip saddr @Client_SSH_IPv4 counter packets 0 bytes 0 accept iif "lo" accept counter log prefix "Input Dft DROP " drop } chain Raw { type filter hook prerouting priority -300; policy accept; tcp dport 36 set add ip saddr @Step1 drop ip saddr @Step1 tcp dport netstat set add ip saddr @Step2 drop ip saddr @Step2 tcp dport nameserver set add ip saddr timeout 10s @Client_SSH_IPv4 drop } }
For more information on nftables feel free to visit the Nftables wiki.