I have a problem with my nftables setup.

I have two tables, each one has a chain with the same hook but a different name and priority.

The tables are in different files which are loaded by an include argument.

Because of the priority, I would think that the VPN-POSTROUTING chain will be executed before the INTERNET chain. But in my setup, the INTERNET chain is executed first.

table ip nat {
        chain INTERNET {
                type nat hook postrouting priority srcnat + 1; policy accept;
                oifname br2 masquerade
table ip vpn {
        chain VPN-POSTROUTING {
                type nat hook postrouting priority srcnat - 1; policy accept;
                oifname br2 ip saddr ip daddr accept

where is my mistake?

Edit: I changed the rules and add all chains to the same table, with the same result.

In the next step, I followed A.B.'s advice and add counters and logs to the rules.

The order of the chains corresponds to the priority, but the accept rule for the VPN is not triggered.

When I add the VPN accept rule to the INTERNET chain, right before the masquerade rule, it works like expected.

  • Add a few debug counter rules. The logical explanation is that the VPN-POSTROUTING rule doesn't match or didn't exist when INTERNET initially matched. Also make sure you don't have the kernel module br_netfilter loaded (eg: by Docker), it can only contribute to confusion. See also: unix.stackexchange.com/questions/650009/…
    – A.B
    Commented Oct 1, 2021 at 13:08
  • Then it could be that each chain has a chance to alter a lookup entry, and since the first didn't take the chance, the second did. You can add more debug: add a new nat chain (in any table) at priority srcnat - 2 with a single rule: nftrace set 1. then use nft monitor trace to confirm all 3 chains were traversed in correct order. Won't solve your problem but will solve the letter of the question.
    – A.B
    Commented Oct 6, 2021 at 12:50
  • Also behavior might have changed before and after kernel 4.18. You should provide the kernel version.
    – A.B
    Commented Oct 6, 2021 at 12:51
  • Thanks a lot. It seems not to be a priority-problem. I also updated the kernel to version 5.11, with the same behavior. When I ping the VPN client, the log has entries for all chains in the right order. The counter is also correct, the counter of the incoming and outcoming packages of the VPN chain are the same, and the counter of the incoming packages of the src-nat chain is also equal to outcoming packages of the VPN chain.
    – smhrambo
    Commented Oct 7, 2021 at 15:25

1 Answer 1


Historically there used to be one NAT chain in a given hook (prerouting, input, output, ...). Executing a nat statement or simply accept-ing the packet being terminal for the chain, with a single chain it was also ending treatment within the hook. With nftables allowing to use more than one chain in the same hook, terminating a chain will just continue to the next chain. So if the first chain doesn't do anything (by accept-ing), the next chain gets its chance to do it instead, which is not what is intended.

To solve this, the first chain (or any other chain) can leave a message passed to each next chain so it can act upon it: set a mark (an arbitrary value) for the next chain to change its behavior.

Instead of accept which has zero effect (leaving the chain VPN-POSTROUTING empty also executes the default policy: accept), set a mark. So replace the rule in VPN-POSTROUTING with this one instead:

nft add rule ip vpn VPN-POSTROUTING oifname br2 ip saddr ip daddr mark set 0xdeaf

When this mark is set, it can then be used on the other chain to change behavior by not executing remaining rules. Insert this rule first in ip nat INTERNET:

nft insert rule ip nat INTERNET mark 0xdeaf accept

You must log in to answer this question.

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