Filter Local Traffic by Process with tc cgroup

The tc cgroup filter sends locally generated packets to a class based on which process, and which net_cls group, sent them. You'll label processes with a Linux net_cls control group, then watch the filter route their traffic.

Allow 20 to 30 minutes for a first test. This needs root privileges, a working classful qdisc, and a system that can mount the legacy cgroup v1 net_cls controller. The tools here are iproute2 6.1.0 on a 6.8.0 Ubuntu kernel; the tc-cgroup manual is dated 21 October 2015, so check your own kernel and iproute2 versions before you build anything long-lived on this.

1. Check that this workflow fits the host

The classifier reads the class ID assigned to the net_cls cgroup of the process that originated a packet, so it's for locally generated traffic only. It won't classify forwarded packets just because they pass through the machine.

Check the command, kernel module and cgroup mounts before changing anything:

$ tc -V
tc utility, iproute2-6.1.0, libbpf 1.3.0
$ modinfo cls_cgroup | sed -n '1,3p'
filename:       /lib/modules/6.8.0-139-generic/kernel/net/sched/cls_cgroup.ko.zst
license:        GPL
description:    TC cgroup classifier
# findmnt -t cgroup,cgroup2 -o TARGET,FSTYPE,OPTIONS

Checkpoint: The manual's setup needs a separate v1 mount at /sys/fs/cgroup/net_cls. A host with only a cgroup2 mount at /sys/fs/cgroup won't have that directory or its net_cls.classid files. If the v1 controller isn't available, stop, don't improvise a cgroup2 replacement for this filter.

2. Create a net_cls cgroup

Loading the module and mounting a controller are elevated operations. This creates a private hierarchy and a group named web-egress:

# modprobe cls_cgroup
# mkdir -p /sys/fs/cgroup/net_cls
# mount -t cgroup -o net_cls net_cls /sys/fs/cgroup/net_cls
# mkdir /sys/fs/cgroup/net_cls/web-egress

Verify the mount and group before assigning any process:

# findmnt /sys/fs/cgroup/net_cls
TARGET                   SOURCE FSTYPE  OPTIONS
/sys/fs/cgroup/net_cls   net_cls cgroup  rw,relatime,net_cls
# test -d /sys/fs/cgroup/net_cls/web-egress && echo ready
ready

Skip the mount command if that hierarchy is already mounted. Reusing an existing controller mount avoids a confusing duplicate setup and makes cleanup safer later.

3. Give the group a class ID

Write the class ID to net_cls.classid as a hexadecimal 64-bit value: the upper 32 bits are the major handle, the lower 32 bits the minor handle. So 1:2 becomes 0x10002, leading zeroes are optional.

# printf '%s\n' 0x10002 > /sys/fs/cgroup/net_cls/web-egress/net_cls.classid
# cat /sys/fs/cgroup/net_cls/web-egress/net_cls.classid
0x10002

This value has to match a class that exists below the qdisc you'll attach later. Writing a class ID neither creates that class nor shapes anything by itself.

4. Put a test process in the group

Pick a process whose network traffic you can recognise. Moving a PID changes its cgroup membership, which can affect other cgroup-based policy, so use a disposable test process first:

$ sleep 300 &
[1] 1234
# printf '%s\n' 1234 > /sys/fs/cgroup/net_cls/web-egress/tasks
# cat /sys/fs/cgroup/net_cls/web-egress/tasks
1234

The PID above is just an example, use the one your shell actually printed. Writing a PID to a group's tasks file moves that process into it. If the process has already exited, its PID is a dead end for testing.

5. Attach the cgroup filter to an existing class

The filter is a classifier hint. It needs a classful qdisc and a class with handle 1:2, so inspect the device first:

# tc qdisc show dev eth0
# tc class show dev eth0

Swap eth0 for the interface whose local traffic you're testing. Only create the qdisc and class if they don't already exist and you own the interface's traffic policy with a tested rollback plan, a qdisc change can alter service behaviour for every flow on the device.

Once class 1:2 exists beneath the selected root, add the filter:

# tc filter add dev eth0 parent 1: protocol ip prio 10 cgroup
# tc filter show dev eth0 parent 1:
filter protocol ip pref 10 cgroup

Exact display formatting varies with kernel and iproute2 version. The checkpoint is simply that a cgroup filter appears under the intended parent. There's no positional PID option on this filter: the class comes entirely from the net_cls assignment on the packet's originating process.

6. Test, then remove the experiment

Generate traffic from the process in the group and check the counters:

# tc -s class show dev eth0
# tc -s filter show dev eth0 parent 1:

Use a controlled request or whatever test suits your network. Counters show packets were accounted for, not that an application-level request actually succeeded. Confirm the class and filter on the correct interface, and remember that processes outside web-egress never get class 1:2 through this filter.

Before leaving the test host, remove the filter and process assignment. Both are state-changing and need root:

# tc filter del dev eth0 parent 1: protocol ip prio 10
# printf '%s\n' 1234 > /sys/fs/cgroup/net_cls/tasks
# rmdir /sys/fs/cgroup/net_cls/web-egress
# umount /sys/fs/cgroup/net_cls

Recovery: Use the same interface, parent and priority you used when adding the filter. If the group holds more processes, move each one out before removing the directory. Don't remove a qdisc or class that was part of an existing policy, and only unmount the hierarchy once the group is empty and you created it solely for this test.

Common traps

Done means