OP uses this kind of rules, that's the layout I'll use as example to base the answer upon:
add rule ip nat postrouting oifname $inet_if ip saddr 172.xx.xx.xx counter snat to $inet_if_ip
add rule ip nat postrouting oifname $inet_if ip saddr 172.xx.xx.xx counter snat to $inet_if_ip
...and so on...
Using a set
nftables features sets, something similar to the ipset companion to iptables, but more generic.
It's thus possible to factorize the nat rules into one rule and then manipulate only the associated set without touching the rule anymore. The manipulation of set elements, including deletion, doesn't need any handle because contrary to rules its content order is irrelevant so it can be efficiently searched and reordered in kernel memory using a hashed list.
If counting independent nat flows (ie: keeping the role of the counter
is important, this requires recent tools (at least >= 0.9.1 but nftables >= 0.9.4 is recommended, see my answer on this, but also see "bugs" at the end of this answer). nftables is still evolving, so some features are dependent of the version of nftables or the kernel.
nftables 0.9.6 0.9.8 was used in this answer. This can affect some syntax and features (like JSON).
So instead of this kind of ruleset:
define inet_if = eth0
define inet_if_ip = 192.0.2.2
table ip nat # for idempotency
delete table ip nat # for idempotency
table ip nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname $inet_if ip saddr 172.17.1.2 counter snat to $inet_if_ip
oifname $inet_if ip saddr 172.17.1.3 counter snat to $inet_if_ip
}
}
one can use a set that can be populated later (or could be directly too):
define inet_if = eth0
define inet_if_ip = 192.0.2.2
table ip nat
delete table ip nat
table ip nat {
set curfewlist {
type ipv4_addr
counter # optional
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname $inet_if ip saddr @curfewlist snat to $inet_if_ip
}
}
The set can then be populated in crontab at 06:00 with (following the previous example):
nft 'add element ip nat curfewlist { 172.17.1.2, 172.17.1.3 }'
At 23:00, It can then be deleted element by element:
nft 'delete element ip nat curfewlist { 172.17.1.2 }'
nft 'delete element ip nat curfewlist { 172.17.1.3 }'
or multiple elements at once:
nft 'delete element ip nat curfewlist { 172.17.1.2, 172.17.1.3 }'
or simply flushed:
nft flush set ip nat curfewlist
Note: the scope (aka the namespace) of a set is within its table: a set can't be referenced from an other table, but contrary to iptables a table isn't limited to one hook type. One can and probably should have multiple types of chains in the same table, allowing to reuse the same set for example in a filter rule and in a nat rule, rather than mimicing iptables' limit of one hook type per table.
Using the meta hour
match
With kernel >= 5.5, there's not even need to alter anything anymore, with the use of meta hour
that can do the check in the packet path. The single nat rule can be replaced with:
meta hour 06:00-23:00 oifname $inet_if ip saddr @curfewlist snat to $inet_if_ip
And the set left permanently filled. Only twice a year, during Daylight Saving Time changes should the ruleset be reloaded, since the kernel always works with UTC time. The complete ruleset would be:
define inet_if = eth0
define inet_if_ip = 192.0.2.2
table ip nat
delete table ip nat
table ip nat {
set curfewlist {
type ipv4_addr
counter
elements = { 172.17.1.2, 172.17.1.3 }
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
meta hour 06:00-23:00 oifname $inet_if ip saddr @curfewlist snat to $inet_if_ip
}
}
Note: the hour interval stored in kernel's memory is in UTC timezone and always a single continuous interval modulo 24h. Depending on the local timezone, the hour expression could be read back differently, like != "23:00"-"06:00"
, which means the same as "06:00"-"23:00"
.
While I think above simplifies the result, below I'll present how to work around or still try and use handles in other ways.
Avoiding the need to manipulate a handle
If methods above aren't adequate for the use case, you can still move all logical rules in an user chain and do operations at the chain level instead of the rule level. This gives here:
define inet_if = eth0
table ip nat
delete table ip nat
table ip nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname $inet_if jump curfew
}
chain curfew { }
}
at 06:00 one would load the following ruleset part (eg with: nft -f curfew.nft
):
define inet_if_ip = 192.0.2.2
flush chain ip nat curfew #for idempotency
table ip nat {
chain curfew {
ip saddr 172.17.1.2 counter snat to $inet_if_ip
ip saddr 172.17.1.3 counter snat to $inet_if_ip
}
}
and at 23:00 simply run this command:
nft flush chain ip nat curfew
Any other method to still use the handle?
This requires one way or an other the use of scripts rather than only nft.
Trying to identify rules later requires to dump all the chain's rules (ie: transfer them from kernel to userland). This is what nftables' developpers have been trying to avoid from the start.
If you plan to add rule that might be deleted later in a given use case, store their handle when they are added, that way there won't be the need to lookup the handle later.
Using --echo
along the --handle
option will display back atomically what was just added, including the handle (running separately nft --handle monitor
would also work). With the previous curfew.nft
file that would give (with the handles changing at each invocation):
# nft --echo --handle -f curfew.nft
add chain ip nat curfew # handle 2
add rule ip nat curfew ip saddr 172.17.1.2 counter packets 0 bytes 0 snat to 192.0.2.2 # handle 4
add rule ip nat curfew ip saddr 172.17.1.3 counter packets 0 bytes 0 snat to 192.0.2.2 # handle 5
So running again with a reasonable filter (made easier by knowing what is added in this specific ruleset part):
# nft --echo --handle -f curfew.nft | sed -n 's/^add rule.*# handle \(.*\)$/\1/p' | tee curfew-handles.txt
6
7
One can append manually an additional rule and also store its handle:
# nft --echo --handle add rule ip nat curfew ip saddr 172.17.1.4 snat to 192.0.2.2 | sed -n 's/^add rule.*# handle \(.*\)$/\1/p' | tee -a curfew-handles.txt
8
and later delete all the rules by handle in the list:
# for i in $(cat curfew-handles.txt); do printf 'delete rule ip nat curfew handle %d\n' $i; done | nft -f - && : > curfew-handles.txt
UPDATE: better JSON support with nftables >= 0.9.8
nftables supports JSON output, but requires version 0.9.8: nft --echo --handle --json add rule ...
echoes back nothing on version 0.9.6, but works properly on 0.9.8 (and tested here with kernel 5.10.x).
Example to retrieve the handle of the rule just added, using jq
. The additional output comment line itself isn't JSON output so has to be filtered out:
# nft --echo --json add rule ip nat curfew ip saddr 172.17.1.4 snat to 192.0.2.2 |
grep -v '^#' |
jq '.nftables[].add.rule.handle'
40
Or using nft monitor
at the same time, while providing chain information along the rule, to be more useful in this context:
nft --json monitor |
grep --line-buffered -v '^#' |
jq -j '
.add.rule |
if . != null then
(.handle, " ", .family, " ", .table, " ", .chain, "\n")
else
empty
end'
Result when the previous rule command above is made:
40 ip nat curfew
Caveat:
bugs in some nftables versions
currently (v0.9.8), using --echo --handle
on some complete rulesets can cause nft
to emit warnings (when the ruleset right before was empty) or even to crash (eg: it appears the quite recently introduced counter
keyword in sets will crash such command or a running nft --handle monitor
). For now better stick to using this on commands simply adding rules.