Route fwmarks into tc Classes with tc-fw
The tc fw classifier reads a firewall mark and sends the matching packet to a traffic control class. This guide marks packets, matches the mark, and sends matching traffic to a class on an existing classful qdisc. Examples use iproute2 6.1.0, installed here as Debian package iproute2 6.1.0-1ubuntu6.4.
The route
Jump straight to the step you need, or tick off Done means at the end.
- Allow about 15 minutes for a live rule, plus time to confirm the mark is applied at the hook you chose.
- The interface needs a classful qdisc already, with a class such as
1:10. This filter does not create a qdisc or a class. - Root only for state-changing commands. The inspection commands can normally run as an ordinary user.
1. Check the installed tools and qdisc
Identify the interface and check what is already attached to it. Replace IFACE with a real interface name, such as enp0s31f6.
$ command -v tc iptables
/usr/sbin/tc
/usr/sbin/iptables
$ tc -V
tc utility, iproute2-6.1.0, libbpf 1.3.0
$ tc qdisc show dev IFACE
$ tc class show dev IFACE
Look for the parent handle and destination class you intend to use. In the commands below, 1: is the parent qdisc handle and 1:10 is an existing class. If the class is missing, stop here and configure the qdisc according to its own documentation; do not guess a class identifier.
Checkpoint
Write down the output of both inspection commands. You will use it to verify the filter and to restore the previous state if needed.
2. Choose where the mark is set
The fw filter compares the packet's 32-bit firewall mark with its handle. A mark can come from an iptables MARK rule, a connection mark, or a tc skbedit action. Keep the marking hook and the filter's direction aligned: an iptables rule in PREROUTING marks arriving packets, while an OUTPUT rule marks locally generated ones.
Warning
This is a live firewall change. Review the rule order first, and do not paste it into a production firewall without a rollback command ready.
# iptables -t mangle -A OUTPUT -o IFACE -j MARK --set-mark 6
# iptables -t mangle -S OUTPUT
The second command should show the new rule with mark 6. That is a packet mark, not a class identifier; the fw filter uses it to select the class later. If you already have a trusted marking policy, do not add a duplicate rule: check it with iptables -t mangle -S and pick a mark that will not collide with another meaning in your policy.
3. Add a filter for the complete mark
Attach the filter to the parent qdisc, using a priority not already taken by another filter:
# tc filter add dev IFACE parent 1: protocol ip prio 10 handle 6 fw classid 1:10
# tc filter show dev IFACE parent 1:
This says: for IPv4 traffic, when the mark matches handle 6, classify the packet into 1:10. With no mask, all 32 mark bits participate, so a mark of 7 does not match handle 6. The filter only classifies; it does not set the mark and does not itself shape traffic.
Expected output varies with the kernel and tc formatter, but it should contain a filter with protocol ip, priority 10, handle 0x6 or 6, and classid 1:10. If the add reports that the parent or class does not exist, go back to step 1. If it reports a duplicate priority or handle, inspect existing filters before changing anything.
4. Match selected bits instead of the whole mark
Reserve bits when several policy decisions share one mark. The mask is written after the handle as HANDLE/MASK. This example matches bit 3 only, regardless of the other bits in the mark:
# tc filter add dev IFACE parent 1: protocol ip prio 20 handle 0x8/0x8 fw classid 1:10
# tc filter show dev IFACE parent 1:
Both the packet mark and the handle are masked before comparison, so marks 0x8 and 0x18 match this filter, while 0x4 does not. Choose non-overlapping masks and priorities if separate classes use separate bits.
You can set a bit from traffic control with skbedit, but that action must run before a later fw filter can see the changed mark:
# tc filter add dev IFACE parent 1: protocol ip prio 5 flower ip_proto tcp action skbedit mark 0x8/0x8
# tc filter add dev IFACE parent 1: protocol ip prio 20 handle 0x8/0x8 fw classid 1:10
The first rule is an example of an action chain, not a substitute for deciding your traffic policy. Test it on a maintenance interface or an isolated path first. If your installed kernel or tc build rejects the flower expression, use the mark from your existing firewall policy instead.
5. Verify counters and direction
Inspect the filter with statistics after generating traffic that should carry the mark:
$ tc -s filter show dev IFACE parent 1:
$ tc -s class show dev IFACE
Filter statistics should show increasing packet and byte counts, and the selected class should show matching traffic. Zero counters usually mean one of four things: the mark was never set, the marking rule ran at a different hook, the protocol was not the one the filter selects, or another filter at an earlier priority claimed the packet first.
Do not infer success from the presence of a filter alone. A filter can be installed correctly and still see no matching packets. Check the marking rule and its counters with iptables -t mangle -L OUTPUT -v -n, or inspect the relevant chain for your traffic direction.
6. Remove only the rules you added
Removal is a state-changing operation. Delete the exact tc filters by their priority and handle, then remove the exact iptables rule. Do not flush a chain or delete the whole qdisc just to undo this example.
# tc filter del dev IFACE parent 1: protocol ip prio 10 handle 6 fw
# tc filter del dev IFACE parent 1: protocol ip prio 20 handle 0x8/0x8 fw
# iptables -t mangle -D OUTPUT -o IFACE -j MARK --set-mark 6
# tc filter show dev IFACE parent 1:
# iptables -t mangle -S OUTPUT
If you used the skbedit example, remove its filter separately by the type and priority you installed. Keep the qdisc and its classes in place unless you mean to remove all traffic-control policy on the interface: a qdisc replacement or deletion can change live packet handling.
Common traps
- A firewall mark is not automatically a classid.
classidis the destination class;handleis the value to match. - The default mask is all 32 bits. Use an explicit mask when only selected bits carry policy.
- The filter does not create
1:10. Confirm the class exists before adding it. - Priority controls filter order. An earlier matching filter can stop this one from ever classifying a packet.
- Do not sudo just to inspect counters. Reserve elevation for the commands that actually alter kernel or firewall state.
Done means
- Parent qdisc and destination class confirmed before the filter was added.
- The mark is set at a hook that sees the traffic being classified.
tc -s filter showandtc -s class showshow matching counters increasing.- Any bit-mask policy has documented, non-overlapping bits and filter priorities.
- The exact tc and iptables undo commands are recorded, and the original qdisc remains intact.