A queueing policy is only as good as the filter that feeds it, and tc-u32 is the classic way to sort packets by their header. You will add a tc-u32 filter that recognises an IPv4 source subnet and sends matching packets to an existing traffic-control class. You will then inspect the filter, understand the compact rule that the kernel creates, and remove the change cleanly. Allow about fifteen minutes, including time to identify the correct qdisc and class.
The examples use the installed iproute2 package, version 6.1.0-1ubuntu6.4, whose tc reports iproute2 6.1.0. The local manual page is dated 25 September 2015. Exact output and hardware-offload behaviour can differ between kernel, iproute2 and interface combinations.
Start with read-only checks. Replace IFACE with the interface carrying the traffic and PARENT with the parent handle of the qdisc where the filter belongs:
$ command -v tc
/usr/sbin/tc
$ tc -V
tc utility, iproute2-6.1.0, libbpf 1.3.0
$ tc qdisc show dev IFACE
$ tc class show dev IFACE
A classifier does not create a qdisc or a class for you. The destination class must already exist, and its class ID must belong to the qdisc named by PARENT. Both the inspection and the later change normally need elevated privileges, so use sudo on a host where your account cannot read or change traffic-control state:
$ sudo tc qdisc show dev eth0
$ sudo tc class show dev eth0
Checkpoint: write down the real interface, parent handle and destination class. Do not use the literal placeholders in a production command.
Before changing a live interface, warn anyone relying on its queueing policy. A filter can alter which class receives packets, so a typo can change latency or bandwidth allocation. The command below follows the manual's IPv4 source-subnet example. It matches traffic from 192.168.8.0/24 and classifies it as 1:1:
$ sudo tc filter add dev eth0 parent 999:0 prio 99 protocol ip u32 \
match ip src 192.168.8.0/24 classid 1:1
Change all four target values together when adapting it: eth0 is the device, 999:0 is the qdisc parent, 99 is the filter priority, and 1:1 is the class ID. The protocol ip restriction makes this an IPv4 rule. The lower the priority number, the earlier filters at the same parent are consulted.
The operation is not reversible by pressing Ctrl-C after it succeeds. Keep the same device, parent, protocol and priority in your notes so that you can remove the rule if it behaves unexpectedly.
Ask tc to display filters attached to the parent. This is a read-only check, although access may still require sudo:
$ sudo tc filter show dev eth0 parent 999:0
filter parent 999: protocol ip pref 99 u32
filter parent 999: protocol ip pref 99 u32 \
fh 800: ht divisor 1
filter parent 999: protocol ip pref 99 u32 \
fh 800::800 order 2048 key ht 800 bkt 0 \
match c0a80800/ffffff00 at 12 flowid 1:1
Your formatting may differ, and the automatically chosen handle need not be 800. The useful facts are that the filter is under the expected parent, has priority 99, matches the IPv4 source prefix, and points to class 1:1. The final line is the low-level form of the higher-level source match: an IPv4 source address begins at byte offset 12, and the mask keeps the first three bytes.
To check counters, include statistics:
$ sudo tc -s filter show dev eth0 parent 999:0
Generate or observe known matching traffic only if doing so is safe for the service. A packet counter that remains at zero can mean that no matching traffic passed, the parent is wrong, the filter is not on the path you expected, or another earlier rule handled the packet.
u32 can be used as a simple ordered list, as in the previous command, or as a collection of hash tables. The manual says that a one-bucket hash table is created for each priority even when you did not request a larger table. That is why tc filter show can print an extra ht divisor 1 line.
For many filters, a custom table can reduce repeated matching work. This example creates a 256-bucket table with handle 1::
$ sudo tc filter add dev eth0 prio 99 handle 1: u32 divisor 256
The divisor must be a power of two, with an exponent no greater than eight, so 256 is the largest value accepted by the documented rule. A linked classifier can then select a bucket:
$ sudo tc filter add dev eth0 parent 1: prio 1 u32 \
link 1: hashkey mask 0x0000ff00 at 12 \
match ip src 192.168.0.0/16
This is an advanced layout, not a prerequisite for one subnet. The link option delegates to a hash table, while hashkey selects packet data used for the bucket lookup. Start with the simple classifier until its parent, class and counters are proven.
Removing a filter changes live classification immediately. First capture the current state so you can distinguish your rule from other rules:
$ sudo tc filter show dev eth0 parent 999:0
For a test parent containing only the example rule, remove filters at that parent and priority:
$ sudo tc filter del dev eth0 parent 999:0 protocol ip pref 99
$ sudo tc filter show dev eth0 parent 999:0
Do not run that deletion blindly on a shared parent: it can remove every matching filter at that priority, not merely the line you intended. If the parent contains other work, stop and use the handles and exact layout shown by your local tc filter show output to plan a narrower change. The same caution applies to removing a custom hash table and its linked filters. Remove dependent filters first, then verify that the table is gone.
classid selects a destination; it does not create one. Check tc class show first.tcp, udp or raw offsets are safe for every packet.skip_sw requires hardware offload and fails if the filter cannot be offloaded. skip_hw prevents hardware processing and leaves software processing in use.tc filter show confirms the parent, priority, match and class.