4

I've migrated from iptables to nftables and have one annoying problem. In the old system I had a script which removed/added a few rules each day. I was able to easily add/delete iptables rules by their position, which was CONSTANT.

Now we have annoying handles. When I add a rule from the script, it gets assigned by dynamic handle number. I can delete rule by retrieving this handle number at first. The problem is that when the same rule gets deleted and re-added. after that, its handle number gets incremented, so is not the same. So I'm not able to use the same simple approach as I had in my old system.

Can anyone give me a solution for this problem - chain has at least 40 rules in it, and I want to delete re-add the same rule several rules each day. It was so easy with iptables, and now I can't find easy solution

Thanks.

8
  • 2
    Can you give examples what these rules are (nft commands or ruleset dump), so that better understanding for giving answer is provided? Thanks.
    – raspi
    Sep 17, 2020 at 15:22
  • There are surely other ways than trying to handle the ... handle. But this requires seeing an example of your ruleset first, as raspi already asked. And an explanation of the need to frequently change this rule (eg: some dynamic DNS resolution requiring to update an IP address?).
    – A.B
    Sep 17, 2020 at 17:42
  • 1
    sorry but you describe how you intend to do it, not why you intend to do it. Can you explain why? Explaining how you think it should be done without why can lead to the XY problem: meta.stackexchange.com/questions/66377/what-is-the-xy-problem
    – A.B
    Sep 18, 2020 at 11:45
  • 1
    If the rules are either about IP addresses or UIDs (which can be displayed as user names too) yes I would like to know, and even see them. Please bear with me. Switching to a newer tool usually requires switching older use habits to newer use habits which are not always the same.
    – A.B
    Sep 20, 2020 at 11:22
  • 1
    The reason it lacks this feature is about how nftables works vs iptables. iptables, to add or remove 1 rule actually replaces the whole ruleset every time. To add the 1000001th rule, iptables download the 1000000 rules, add one, then sends back to the kernel 1000001 rules: quite slow to add one rule. nftables sends only the delta of one rule to the kernel. That's related to the lack of the feature you're looking for.
    – A.B
    Sep 20, 2020 at 11:28

4 Answers 4

5

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.

1

This could work:

handle=$(nft list ruleset -a | grep "$your_rule" | grep "#" | awk '{print $NF}')
test -n "$handle" && nft delete rule filter output handle "$handle"
nft insert rule filter output position XX ...
4
  • Don't you feel the irony - I was able to do this with the simple iptables command and now I need to use script for this? :) And this will not work completely as I have at least 40 very similar rules in nat table and I want to remove for example second, sixth and eight rule which differs slightly.
    – Rimvydas
    Sep 18, 2020 at 6:31
  • You can still use iptables. I believe they will be here for at least ten more years. Sep 18, 2020 at 9:42
  • 1
    Yeah, I can use iptables, but I thought that nftables is better version of iptables. And it is very strange that they lack such needed feature:(
    – Rimvydas
    Sep 20, 2020 at 11:15
  • Open Source is mostly developed by the "I need this feature" basis where "I" is an open source developer. Then there are companies which pay for features they need. The average user needs are most often dismissed unless the developer also wants to get them. Sep 20, 2020 at 17:11
1

What I would do

Ise a custom table, and replace it each time. Table updates are atomic if done with a single call to nft.

So change the rules in my program, re-generate the table, call nft.

1

Element timeouts may provide the solution you need. You can place the addresses or ports in a set, from which they will be removed automatically upon expiration.

Timeouts can be specified in hours, minutes, seconds or a combination such as 2h10m5s.

Quoting and example from the nftables wiki:

% nft add table inet myfilter
% nft add set inet myfilter myset {type ipv4_addr\; flags timeout\; }
% nft add element inet myfilter myset {10.0.0.1 timeout 10s }

Result:

% nft list ruleset
table inet myfilter {
    set myset {
        type ipv4_addr
        flags timeout
        elements = { 10.0.0.1 timeout 10s expires 8s}
    }
}

Source: https://wiki.nftables.org/wiki-nftables/index.php/Element_timeouts

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .