How can do use nftables for port knocking _and_ NAT?

Now I know port knocking isn’t too clever; a VPN would be better. But on my phone I use blockada, which acts as a VPN to block ads. OpenVPN or Tailscale can’t run at the same time as blockada. But I could portknock and allow the phone to access the protected resource and have blockada at the same time. I’m aware of the risks :slight_smile:

So the question is “how can I do this?” I have a rocky9 server acting as my router and I have a bunch of nftables rules that I worked out with help from this forum ( Building a home router · Ramblings of a Unix Geek ) .

What I would like to do is have a port knocking sequence that would then allow the router to forward port 12345 to 10.0.0.2:993 (for example). I have maybe 4 of these ports I want to protect this way.

I see a number of “how to use nftables for port knocking” pages ( eg Port knocking example - nftables wiki ) but these all seem to be to allow access to ports on the server (eg the router).

But I haven’t seen how to do this for the “ip nat” chain.

Any ideas?

You surely know the basics of port forwarding:

  • Redirect (DNAT) incoming packet to new destination
  • Allow routed packets to pass through
chain nat_PREROUTING {
	type nat hook prerouting priority dstnat; policy accept;
	tcp dport 12345 other_conditions dnat to 10.0.0.2:993
}

chain filter_FORWARD {
	type filter hook forward priority filter; policy accept;
	ct state established,related accept
	ct status dnat accept
	iifname "lo" accept
	...

In the above the “allow passthrough” is handled by the ct status dnat accept

Who is redirected gets decided by the conditions:

  • tcp dport 12345
  • other_conditions

The other_conditions could include incoming interface, source address of the packet, etc.

Your example of port knocking had a rule:

tcp dport $guarded_ports ip  saddr @clients_ipv4 counter accept

That is, two conditions:

  • tcp dport 12345
  • ip saddr those_who_have_knocked_recently

You surely can have the ip saddr @clients_ipv4 condition in your dnat rule too?

My rules are a bit more complicated than that. In particular the simple rules don’t allow for “reflection” (ie an internal host talking to the WAN IP address). And I use a map of destinations.

Something like


add map nat fport { type inet_service : interval ipv4_addr . inet_service ; flags interval; }
add set ip nat reflect { type inet_service; }

...

add element nat fport { 11111 : 10.0.0.2 . 22 }   # ssh
...
add element nat reflect { ..., 11111, ... }
...
# Allow marked traffic (ie reflected traffic) to reach the destination
add rule ip filter FORWARD meta mark 100 counter accept

# Tag internal reflection traffic
add rule ip nat PREROUTING ip saddr 10.0.0.0/8 ip daddr @this_host tcp dport @reflect mark set 100

# DNAT incoming traffic from the internet (or internal sent to br-wan address)
add rule ip nat PREROUTING ip daddr @this_host ip protocol tcp dnat ip  addr . port to tcp dport map @fport

# Do NAT on egress traffic to the internet
add rule ip nat POSTROUTING oifname "br-wan" counter masquerade

# Also NAT marked traffic from internal to external IP
add rule ip nat POSTROUTING meta mark 100 counter masquerade

Working out those reflection rules was the hard part.

These rules allow access to ports I always want available (eg http, https, ssh). I’m now wanting a second set of ports that are only exposed when port-knocked. I’m guessing I’m gonna need another map “hidden_ports” (or something) for mapping 12345 to 10.0.0.2:993.

But that’s where I’m not sure how to add that additional condition. Would it be as simple as

add rule ip nat PREROUTING ip daddr @this_host ip  saddr @clients_ipv4 ip protocol tcp dnat ip  addr . port to tcp dport map @hidden_ports

ie just adding ip saddr @clients_ipv4 to a copy of the existing “fport” rule (modified for “hidden_ports”) ?

Or is there more to it?

I have never tried, but that is the impression that all the examples do give.

Have you tried this ?
Server-side TCP port knocking with Linux nftables

Yeah, I had seen that. It’s pretty much the same as before; it’s on the “input” chain, so would be for services running on the device implementing the port knocking. I’m wanting to do this for the “nat” chain.

Here they suggest using the iptables to nft converter.
working version with iptables dnat knocking to nft .
Nftables port knocking dnat
Try converting and see what nft rules it will convert to.

Yeah, I’ve seen that as well. And I’ve used the translate before to get an idea of how to do things (and then ignored it :-)). But in this case the answer to the question doesn’t even work.

For example, in the source there was a 3389, and we can see that in the saved output, but the translated version has it commented out because it doesn’t know what to do with it

% grep 3389 knock
$IPT -t nat -A PREROUTING -p tcp --dport 777 -m recent --rcheck --seconds 30 --name RDP -j DNAT --to-destination 192.168.1.254:3389

% grep 3389 knock.save 
-A PREROUTING -p tcp -m tcp --dport 777 -m recent --rcheck --seconds 30 --name RDP --mask 255.255.255.255 --rsource -j DNAT --to-destination 192.168.1.254:3389

% grep 3389 knock.nft 
# -t nat -A PREROUTING -p tcp -m tcp --dport 777 -m recent --rcheck --seconds 30 --name RDP --mask 255.255.255.255 --rsource -j DNAT --to-destination 192.168.1.254:3389

(As it happens, other rules also get commented out, so the whole ruleset is broken)

So because I have two tables; one for standard filters and one for NAT, I had to put the port-knocking rules into the “nat” table INPUT rules.

So, for example

add map nat hidden_ports { type inet_service : interval ipv4_addr . inet_service ; flags interval; }

# For port knocking
add set ip nat knocked  { type ipv4_addr; flags timeout; timeout 3600s;}
add set ip nat knockers { type ipv4_addr . inet_service ; flags timeout; timeout 1s; }

add element nat hidden_ports { 12345 : 10.0.0.2 . 993 }

# Handle port knocking
add rule ip nat INPUT tcp dport 123 add @knockers {ip saddr . 234 }
add rule ip nat INPUT tcp dport 234 ip saddr . tcp dport @knockers add @knockers {ip  saddr . 345 }
add rule ip nat INPUT tcp dport 345 ip saddr . tcp dport @knockers add @knockers {ip  saddr . 456 }
add rule ip nat INPUT tcp dport 456 ip saddr . tcp dport @knockers add @knocked {ip  saddr }

add rule ip nat PREROUTING ip daddr @this_host ip saddr @knocked ip protocol tcp dnat ip addr . port to tcp dport map @hidden_ports

Obviously I’d change the port sequence to something different. In this example we get 1 second between each port knock, and if it succeeds then the port is opened for 1 hour. Once I’d worked out that table name issues, the PREROUTING rule was as simple as adding the extra ip saddr requirement. I’ve tested this version on a test machine (not my live router) but I think it should work!

Not sure why I really need two tables; maybe I can just merge them into one, which would make things a little simpler. Hmm.