It's possible to work around nftables limitations by using a pre-computed lookup named map.
Required:
- kernel >= 5.4: for
meta hour
- nftables >= 0.9.4 for the newer
typeof
that can be used instead of type
and which appears to be the only syntax supported in a map for meta hour
. type meta hour
or type hour
won't be accepted, even if typeof meta hour
is read back as type hour
with an older nftables.
nftables
cannot do arbitrary arithmetic (or logical, etc.) operations. Currently it's limited to performing a few operations on the left hand side (LHS) on data which must come from the packet path or a few extensions like numgen
, and compare it to a constant right hand side (RHS), including getting the RHS value from sets and maps.
There's no way to have nftables compute an arbitrary division in the LHS (using right shift to divide by a power of two works, but see later). And it appears there is also no way to express the result of such computation as a data type accepted as a key in a named map, because the "unqualified" integer type which is often the result of such operations is not a valid key type in a named map, at least currently. For example I'm not sure a numgen
expression can be used with a named map, even if it works with an anonymous map.
For this case, Linux kernel 5.4 introduces meta statements related to the packet's timestamp in kernel:
- Introduce meta matches in the kernel for time, day, and hour commit
Here, meta hour
provides the missing required operation "mod 86400" which is related enough to OP's requirement of "timestamp / 1800": gives the time of the packet since the start of the day. Moreover it has a specific type, which allows it to be used in a named map.
As a method similar to loop unrolling to simplify complex operations, values that can't be computed in expressions can be pre-computed and stored in a map table, as long as the type is valid for a named map. This is acceptable because the number of entries will be known in advance, and still limited: for this case, there are 48 half hours in a day.
The mapping can then provide the final result: an IPv4 address.
Example ruleset for a loadbalancer (doing dnat) which forwards all traffic received on wan0 to 10.10.10.1 the first half hour of each hour and to 10.10.10.2 the second half hour:
table ip mytable
delete table ip mytable
table ip mytable {
map hour2ip {
typeof meta hour : ip daddr
flags interval
}
chain mylb {
type nat hook prerouting priority dstnat; policy accept;
iif wan0 dnat to meta hour map @hour2ip
}
}
Script generating nftables commands to populate the map hour2ip (use ./script.sh | nft -f -
):
#!/bin/sh
for h in $(seq 0 23); do
for m in 0 30; do
printf 'add element ip mytable hour2ip { "%02d:%02d:00"-"%02d:%02d:59" : %s }\n' $h $m $h $((m+29)) '$ip'
done
done |
sed 's/$ip/10.10.10.X/g' |
awk '{ printf "%s\n",gensub("X",(NR-1)%2+1,1) }'
which would generate this kind of commands:
add element ip mytable hour2ip { "00:00:00"-"00:29:59" : 10.10.10.1 }
add element ip mytable hour2ip { "00:30:00"-"00:59:59" : 10.10.10.2 }
[...]
add element ip mytable hour2ip { "23:00:00"-"23:29:59" : 10.10.10.1 }
add element ip mytable hour2ip { "23:30:00"-"23:59:59" : 10.10.10.2 }
notes:
- the local timezone affects the way data of type
meta hour
(which is a modulo) are sent to or displayed back from the kernel which as usual is only using UTC time. That's why ordering will appear shifted depending on the current timezone, but will behave as expected. You can check the effect of using or not the environment variable TZ=UTC
when displaying back nft list map ip mytable hour2ip
to understand it better.
- there's no gap between 00:29:59 and 00:30:00: the timestamp is rounded down, so for example 00:29:59.800ms still matches 00:29:59.
If you prefer to use a mark to get more flexibility (by reusing the mark elsewhere than just the dnat rule, including saving it to a connmark), you could use for example two maps: one to map hour to mark and an other to map mark to an IP address.
To do this replace for example the previous ruleset with:
table ip mytable
delete table ip mytable
table ip mytable {
map hour2mark {
typeof meta hour : meta mark
flags interval
}
map mark2ip {
typeof meta mark : ip daddr
elements = { 1 : 10.10.10.1, 2 : 10.10.10.2 }
}
chain mylb {
type nat hook prerouting priority dstnat; policy accept;
iif wan0 meta mark set meta hour map @hour2mark dnat to meta mark map @mark2ip
}
}
and replace in the previous script generating the entries:
sed 's/$ip/10.10.10.X/g' |
with:
sed 's/hour2ip/hour2mark/;s/$ip/X/g' |
to create the commands to populate hour2mark, like:
add element ip mytable hour2mark { "00:00:00"-"00:29:59" : 1 }
add element ip mytable hour2mark { "00:30:00"-"00:59:59" : 2 }
[...]
add element ip mytable hour2mark { "23:00:00"-"23:29:59" : 1 }
add element ip mytable hour2mark { "23:30:00"-"23:59:59" : 2 }