I would like to change source address of every packet generated by a process in given cgroup (version 2). Is that even possible?

I have:

  • nftables 1.0.2,
  • linux 5.15 (Ubuntu variant)
  • /system.slice/system-my-service.slice/[email protected] cgroup

I have tried to:

  • create a table nft add table ip myservice
  • create a postrouting nat chain nft add chain ip myservice postrouting { type nat hook postrouting priority 100 \; }
  • try to create postrouting rule nft add rule ip myservice postrouting socket cgroupv2 level 1 'system.slice' snat (during experiments, I have used only 'system.slice', because nft has issues with @ in the cgroup name, which would be level 2 issue :-).

I have found also cgroup matching which requires int32 argument, from which I guess it's cgroup version 1 (thus not applicable for me), because I have found no trace of hint for converting the path style cgroup to int.

I suspect the socket expression is not applicable to the postrouting nat chain, as the nft suggests in Error: Could not process rule: Operation not supported.

Do all the fails mean, that this is completely wrong approach? Or have I just missed something obvious?

1 Answer 1


Here's an answer to your two problems:

  • syntax

    cgroupv2 expects a path, which is a string. A string is always displayed with double-quotes, and requires double-quotes if it includes special characters. These double-quotes are for the nft command's consumption, not for the shell. With direct commands (ie: not in a file read using nft -f), these double-quotes themselves should be escaped or enclosed with single-quote, else the shell would consume them.

    In addition this path is documented as relative and doesn't need a leading / (anyway it's accepted and removed when displayed back), giving when directly from shell:

    socket cgroupv2 level 3 '"system.slice/system-my-service.slice/[email protected]"'

    Finally, nft doesn't care if it's given a single parameter with multiple tokens or multiple parameters one token at a time: the line is assembled and parsed the same. So whenever there's a special character for the shell (here "), just single-quote all the line rather than try to figure out where to escape characters (eg \; in base chains).

  • work around the limitation

    You can mark the packet in output hook and check the mark in postrouting hook to do NAT.

    nft 'add chain ip myservice { type filter hook output priority 0; policy accept; }'
    nft 'add rule ip myservice output socket cgroupv2 level 3 "system.slice/system-my-service.slice/[email protected]" meta mark set 0xcafe'
    nft add rule ip myservice postrouting meta mark 0xcafe snat to

The final result can be put in its own file, with the two glue commands at start for idempotence. Having its own file prevents any interaction with shell parsing, and gets atomic updates. It still requires "..." around the path parameter to not having nftables interpret the @ character.


table ip myservice
delete table ip myservice

table ip myservice {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        meta mark 0xcafe snat to

    chain output {
        type filter hook output priority 0; policy accept;
        socket cgroupv2 level 3 "system.slice/system-my-service.slice/[email protected]" meta mark set 0xcafe

which can then be loaded with nft -f myservice.nft, as long as the cgroup already exists (probably meaning, that at boot the service has to be started before nftables loads this rule).

In the end:

  • every outgoing packet will traverse filter/output

    • if it's from a process in the adequate cgroup the packet will receive a mark 0xcafe
  • every first packet of a new flow will traverse the nat/postrouting rule.

    • if it matches the mark 0xcafe (meaning it was in the adequate cgroup) this will trigger the SNAT rule for the flow (then the mark doesn't matter anymore)

You must log in to answer this question.

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