Classify TCP Traffic with tc flower
tc flower matches packets straight in the kernel, so you can tag, redirect or drop traffic without touching iptables at all. This guide adds one reversible TCP filter on the ingress hook, checks how the kernel actually stored it, then removes it cleanly. Allow about 15 minutes, plus time to pick the right interface and decide what matching packets should do.
The route
Jump straight to the step you need, or tick off Done means at the end.
Built against the installed iproute2 package, version 6.1.0-1ubuntu6.4, with tc reporting iproute2-6.1. The local tc-flower(8) page is dated 22 October 2015, so treat these examples as verified on this install rather than every newer release.
1. Check the interface and command
- Pick the real interface. Do not assume
eth0exists; a typo just fails silently, and the wrong live interface can hit traffic you never meant to touch. - Run the inspection commands below. None of them change traffic control state, and none normally need elevated privileges.
$ command -v tc
/usr/sbin/tc
$ tc -V
tc utility, iproute2-6.1.0, libbpf 1.3.0
$ ip -br link
lo UNKNOWN 127.0.0.1/8 ::1/128
enp1s0 UP
2. Inspect the current ingress filters
Before changing anything, record what is already on the chosen interface. ingress means packets arriving at the interface, not packets leaving it.
$ tc filter show dev enp1s0 ingress
No output means nothing is currently visible to tc on this hook. If you do see entries, note their preference numbers and handles. Do not delete the whole hook just to make room for this example.
3. Add one TCP match
Checkpoint
The next command changes packet handling and needs root. Test it on a maintenance window or a disposable interface if the filter will drop, redirect or otherwise alter production traffic. This example uses a pass action, so matching packets carry on through normal processing, and matches IPv4 TCP packets whose destination port is 443.
$ sudo tc filter add dev enp1s0 ingress protocol ip pref 10 flower ip_proto tcp dst_port 443 action gact pass
- Preference is 10. Lower preference values are considered earlier than higher ones, so an existing filter can still win before this one runs.
- protocol ip supplies the layer-three context. The flower manual requires port matches to also carry
ip_protooftcp,udporsctp; puttingip_proto tcpbeforedst_port 443makes that dependency explicit. - action gact pass is a placeholder. For a real policy, swap it only once you have decided whether you need a class, a redirect, policing or a drop. A drop action is service-disrupting, so test that with a narrow source or destination match first.
4. Verify the stored filter
Ask tc to print the ingress filter again. Formatting varies, but it should show the interface, preference 10, the flower classifier, TCP, and port 443.
$ sudo tc filter show dev enp1s0 ingress
filter protocol ip pref 10 flower chain 0
eth_type ipv4
ip_proto tcp
dst_port 443
action order 1: gact action pass
index 1 ref 1 bind 1
Some kernels print fewer fields, and the action index is not something worth copying into a script. What matters is that the filter sits on the right device and hook, the preference is correct, and the match is the one you meant.
For counters, add -s:
$ sudo tc -s filter show dev enp1s0 ingress
Tip
A counter stuck at zero does not mean the rule is broken. It usually means no matching traffic has arrived yet, or an earlier filter already claimed it.
5. Extend the match without losing its prerequisites
You can combine flower keys to narrow a match further. This example adds an IPv4 destination prefix on top of TCP port 443:
$ sudo tc filter add dev enp1s0 ingress protocol ip pref 20 flower src_ip 192.0.2.0/24 ip_proto tcp dst_port 443 action gact pass
- One mask per preference. If two filters at the same preference need different masks, give them different preferences instead.
- A missing prefix length means a full host match. Write
192.0.2.10/32when that exactness matters. - UDP or SCTP need both the port key and the matching ip_proto. ICMP
typeandcodekeys needip_proto icmporicmpv6. Layer-two keys such assrc_macanddst_maccarry no such dependency.
6. Remove the example filter
Checkpoint
Remove the rule once the test is done, or before you change its preference. This is an elevated command, but it is reversible: it deletes only the filter identified by that same device, location, protocol and preference.
$ sudo tc filter del dev enp1s0 ingress protocol ip pref 10 flower
$ sudo tc filter show dev enp1s0 ingress
The second command should no longer show preference 10. If the delete reports no matching filter, compare device, hook, protocol and preference against what you actually have. Do not reach for a broad flush as a shortcut; it can take out unrelated filters with it.
Common failure points
- Operation not permitted: rerun the state-changing command with
sudo, or ask an administrator. Do not grant extra capabilities totcjust to dodge a controlled privilege boundary. - Cannot find device: recheck with
ip -br linkand fix the interface name. Network namespaces matter too: runtcin the namespace that owns the interface. - Invalid argument for a port: check that
ip_protois present and istcp,udporsctp. Port ranges use the documented form, such asdst_port 8000-8100. - Hardware offload surprises:
skip_swneeds hardware support and fails when the filter cannot be offloaded;skip_hwblocks hardware processing entirely. Leave both out while learning, then choose deliberately. - Wrong traffic matched: inspect with
-s, narrow the match, and keep a copy of the pre-change filter output. Never test a drop action on the only management path to a remote host.
Done means
- Interface and hook checked before you touched anything.
- Filter carries the required protocol context for its layer-three and port keys.
tc filter showconfirms the intended preference, match and action.- Statistics checked with
tc -swhen traffic testing was possible. - Temporary rule removed with a narrow delete, not a flush.