TC-U32(8)
๐ TC-U32(8) ๐
Universal 32bit classifier in tc(8) ๐ธ Linux ๐ฆ
โจ NAME โจ
u32 - universal 32bit traffic control filter ๐
๐ SYNOPSIS ๐
tc filter ... [ handle HANDLE ] u32 OPTION_LIST [ offset OFFSET ] [ hashkey HASHKEY ] [ classid CLASSID ] [ divisor uint_value ] [ order u32_value ] [ ht HANDLE ] [ sample SELECTOR [ divisor uint_value ] ] [ link HANDLE ] [ indev ifname ] [ skip_hw | skip_sw ] [ help ]
HANDLE := { u12_hex_htid:[u8_hex_hash:[u12_hex_nodeid] | 0xu32_hex_value }
OPTION_LIST := [ OPTION_LIST ] OPTION
HASHKEY := [ mask u32_hex_value ] [ at 4*int_value ]
CLASSID := { root | none | [u16_major]:u16_minor | u32_hex_value }
OFFSET := [ plus int_value ] [ at 2*int_value ] [ mask u16_hex_value ] [ shift int_value ] [ eat ]
OPTION := { match SELECTOR | action ACTION }
SELECTOR := { u32 VAL_MASK_32 | u16 VAL_MASK_16 | u8 VAL_MASK_8 | ip IP | ip6 IP6 | { tcp | udp } TCPUDP | icmp ICMP | mark VAL_MASK_32 | ether ETHER }
IP := { { src | dst } { default | any | all | ip_address [ / { prefixlen | netmask } ] } AT | { dsfield | ihl | protocol | precedence | icmp_type | icmp_code } VAL_MASK_8 | { sport | dport } VAL_MASK_16 | nofrag | firstfrag | df | mf }
IP6 := { { src | dst } { default | any | all | ip6_address [/prefixlen ] } AT | priority VAL_MASK_8 | { protocol | icmp_type | icmp_code } VAL_MASK_8 | flowlabel VAL_MASK_32 | { sport | dport } VAL_MASK_16 }
TCPUDP := { src | dst } VAL_MASK_16
ICMP := { type VAL_MASK_8 | code VAL_MASK_8 }
ETHER := { src | dst } ether_address AT
VAL_MASK_32 := u32_value u32_hex_mask [ AT ]
VAL_MASK_16 := u16_value u16_hex_mask [ AT ]
VAL_MASK_8 := u8_value u8_hex_mask [ AT ]
AT := [ at [ nexthdr+ ] int_value ]
๐ท DESCRIPTION ๐ท
okay so the Universal/Ugly 32bit filter ๐ lets you match arbitrary bitfields inside a packet. Because everything gets broken down into values, masks and offsets, it is equally powerful and equally painful to use ๐ญ. Luckily there are loads of abstracting directives that let you define rules at a higher level, so you don't have to fiddle with bits and masks in lots of cases ๐ซถ.
There are two general modes of invocation โจ. The first mode creates a new filter to delegate packets to different destinations. Besides the obvious ones, which are classifying the packet by specifying a CLASSID or calling an action, you can link one filter to another one (or even a whole list of them), which basically organises your filters into a tree-like hierarchy ๐ณ.
Filter delegation is usually done with a hash table, and that brings us to the second mode of invocation: it just sets up these hash tables ๐ง. Filters can select a hash table and provide a key selector. A hash gets computed from that and used as the key to look up the table's bucket, which holds filters for further processing ๐ฅ. This is super useful if you have a high number of filters in use, because the overhead of the hash operation and table lookup becomes negligible in that case ๐. Using hashtables with u32 basically follows this pattern:
- Create a new hash table, specifying its size using the
divisorparameter and ideally a handle so the table can be identified. If you don't give the handle, the kernel picks one on its own, which you then have to guess later ๐. - Create filters that link to the table from step 1 using the
linkparameter, and define the packet data the kernel will use to calculate the hashkey. - Add filters to buckets in the hash table from step 1. To avoid having to know exactly how the kernel builds the hash key, there's the
sampleparameter. It gives sample data to hash, which defines the table bucket the filter should be added to ๐ฆ.
Here's the tea โ: even if you don't explicitly ask for it, u32 creates a hash table for every priority a filter is added with. That table's size is 1 though, so it's really just a linked list ๐.
๐ VALUES ๐
Options and selectors need their values in a specific format, which is often non-intuitive ๐. So the terminals in SYNOPSIS got descriptive names that tell you the required format and/or the maximum allowed numeric value. Here's how to read them:
u32,u16andu8prefixes mean four, two and single byte unsigned values. E.g.u16is a two byte-sized value in the range between 0 and 65535 (0xFFFF) inclusive ๐ .intprefix means a four byte signed value โจ._hex_in the middle of a name means the value is parsed as hexadecimal ๐ธ.- Otherwise the base is detected automatically: values prefixed with
0xare hexadecimal, a leading0means octal, and everything else is decimal ๐ง.
There are some values with special formatting too ๐ซถ:
ip_addressandnetmaskuse dotted-quad formatting, as usual for IPv4 addresses ๐.- An
ip6_addressis written in common, colon-separated hexadecimal format ๐. prefixlenis an unsigned, decimal integer from 0 to the address width in bits, which is 32 for IPv4 and 128 for IPv6 ๐.
Sometimes a value has to be dividable by a certain number. For those, a name of the form N*val was chosen, which means val must be dividable by N. Or the other way around: the resulting value must be a multiple of N โญ.
to be continued ๐ฆ
๐ SELECTORS ๐
Okay bestie, the only real selector is u32 ๐
. Everything else is just a cute higher-level syntax that gets quietly translated into u32 behind the scenes ๐ง
u32 VAL_MASK_32, u16 VAL_MASK_16, u8 VAL_MASK_8
Match packet data against a given value โจ. The selector name decides how many bits get grabbed: 32bits for u32, 16bits for u16, and 8bits for u8. Before comparing, the sample gets binary AND'ed with the mask, so you can wipe out the boring bits first ๐. The sample's position comes from the offset you put in AT.
ip IP, ip6 IP6
Assumes the packet starts with an IPv4 (ip) or IPv6 (ip6) header ๐ธ. Then IP/IP6 lets you match all the different header fields:
src ADDR,dst ADDRโ Compares the Source or Destination Address fields againstADDR๐. The reserved wordsdefault,anyandalleffectively match any address. Otherwise you need an IP address of the right protocol, optionally with a prefix length to match whole subnets. For IPv4 you can also give a netmask.dsfield VAL_MASK_8โ IPv4 only ๐ฆ. Matches the packet header's DSCP/ECN field. The synonyms aretosandprecedence.ihl VAL_MASK_8โ IPv4 only. Matches the Internet Header Length field. Heads up: the value's unit is 32bits, so to match a packet with a 24byte header length,u8_valuehas to be6๐ค.protocol VAL_MASK_8โ Matches the Protocol (IPv4) or Next Header (IPv6) field value, e.g.6for TCP.icmp_type VAL_MASK_8,icmp_code VAL_MASK_8โ Assumes a next-header protocol oficmporipv6-icmpand matches the Type or Code field values.
๐ This one is dangerous! The code assumes a minimal header size for IPv4 and no extension headers for IPv6.
sport VAL_MASK_16,dport VAL_MASK_16โ Matches the layer four source or destination ports ๐.
๐ Also dangerous. It assumes a suitable layer four protocol is there (one with Source and Destination Port fields right at the start of the header, 16bit in size). It also assumes minimal header size for IPv4 and no IPv6 extension headers.
nofrag,firstfrag,df,mfโ IPv4 only ๐งฉ. These check certain flags and fragment offset values. Matches if the packet is not a fragment (nofrag), is the first fragment of a fragmented packet (firstfrag), has the Don't Fragment (df) bit set, or has the More Fragments (mf) bit set.priority VAL_MASK_8โ IPv6 only โจ. Matches the header's Traffic Class field. It has the same purpose and semantics as IPv4's ToS field since RFC 3168: the upper six bits are DSCP, and the lower two are ECN.flowlabel VAL_MASK_32โ IPv6 only ๐. Matches the Flow Label field's value. The Flow Label itself is only 20bytes long, and those are the least significant ones here. The remaining upper 12bytes match the Version and Traffic Class fields.
tcp TCPUDP, udp TCPUDP
Matches fields of the next header, which is TCP or UDP ๐. The possible values for TCPDUP are:
src VAL_MASK_16โ Matches the Source Port field value.dst VALMASK_16โ Matches the Destination Port field value.
icmp ICMP
Matches fields of the next header, which is ICMP ๐. The possible values for ICMP are:
type VAL_MASK_8โ Matches the ICMP Type field.code VAL_MASK_8โ Matches the ICMP Code field.
mark VAL_MASK_32
Matches the netfilter fwmark value ๐ฅ.
ether ETHER
Matches ethernet header fields ๐. The possible values for ETHER are:
src ether_address AT,dst ether_address ATโ Matches the source or destination ethernet address.
๐ญ This is dangerous! It assumes an ethernet header is present at the start of the packet, so things can get weird if you use it with layer three interfaces liketunorppp.
โจ EXAMPLES โจ
Okay, first up, the basics ๐ง:
tc filter add dev eth0 parent 999:0 prio 99 protocol ip u32 \
match ip src 192.168.8.0/24 classid 1:1
This attaches a filter to the qdisc identified by 999:0. Its priority is 99, which decides the order multiple filters on the same parent get checked in (the lower the earlier) ๐ข. The filter handles packets of protocol type ip, and matches if the IP header's source address is within the 192.168.8.0/24 subnet. Matching packets get classified into class 1.1. The effect of this command might be surprising at first glance ๐:
filter parent 1: protocol ip pref 99 u32
filter parent 1: protocol ip pref 99 u32 \
fh 800: ht divisor 1
filter parent 1: protocol ip pref 99 u32 \
fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:1 \
match c0a80800/ffffff00 at 12
So parent 1: gets a new u32 filter, which contains a hash table of size 1 (as the divisor indicates). The table ID is 800. The third line shows the actual filter that was added above: it sits in table 800 and bucket 0, classifies packets into class ID 1:1, and matches the upper three bytes of the four byte value at offset 12 to be 0xc0a808, which is 192, 168 and 8 ๐ธ.
Now for something more complicated, namely creating a custom hash table ๐ง :
tc filter add dev eth0 prio 99 handle 1: u32 divisor 256
This creates a table of size 256 with handle 1: in priority 99. The effect is as follows:
filter parent 1: protocol all pref 99 u32
filter parent 1: protocol all pref 99 u32 fh 1: ht divisor 256
filter parent 1: protocol all pref 99 u32 fh 800: ht divisor 1
So along with the requested hash table (handle 1:), the kernel has created its own table of size 1 to hold other filters of the same priority ๐ซถ.
The next step is to create a filter that links to the created hash table:
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
The filter gets a lower priority than the hash table itself, so u32 consults it before manually traversing the hash table ๐. The options link and hashkey determine which table and bucket to redirect to. In this case the hash key is built out of the second byte at offset 12, which corresponds to an IP packet's third byte of the source address field. Along with the match statement, this effectively maps all class C networks below 192.168.0.0/16 to different buckets of the hash table ๐บ๏ธ.
Filters for certain subnets can be created like so:
tc filter add dev eth0 parent 1: prio 99 u32 \
ht 1: sample u32 0x00000800 0x0000ff00 at 12 \
match ip src 192.168.8.0/24 classid 1:1
The bucket is defined using the sample option. In this case, the second byte at offset 12 must be exactly 0x08 ๐ฏ. The resulting bucket ID is obviously 8, but as soon as sample selects an amount of data that could exceed the divisor, you'd have to know the kernel-internal algorithm to deduce the destination bucket ๐ตโ๐ซ. This filter's match statement is redundant here, since the entropy for the hash key does not exceed the table size and therefore no collisions can occur. Otherwise it's necessary to prevent matching unwanted packets.
Matching upper layer fields is tricky ๐ญ, since the IPv4 header length is variable and IPv6 supports extension headers, which affect the upper layer header offset. To get around this, you can specify nexthdr+ when giving an offset, and to make things easier there are the tcp and udp matches, which use nexthdr+ implicitly. The offset has to be calculated beforehand though, and the only way to do that is in a separate filter that then links to the filter which wants to use it. Here's an example of doing so ๐
:
tc filter add dev eth0 parent 1:0 protocol ip handle 1: \
u32 divisor 1
tc filter add dev eth0 parent 1:0 protocol ip \
u32 ht 1: \
match tcp src 22 FFFF \
classid 1:2
tc filter add dev eth0 parent 1:0 protocol ip \
u32 ht 800: \
match ip protocol 6 FF \
match u16 0 1fff at 6 \
offset at 0 mask 0f00 shift 6 \
link 1:
Here's what's happening ๐ง: in the first call, a single element sized hash table is created, so there's a place to hold the linked filter and a known handle (1:) to reference it by. The second call adds the actual filter, which pushes packets with TCP source port 22 into class 1:2. Using ht, it's moved into the hash table created by the first call. The third call does the actual magic ๐ช: it matches IPv4 packets with next layer protocol 6 (TCP), but only if it's the first fragment (usually TCP sets the DF bit, but if it doesn't and the packet is fragmented, only the first one contains the TCP header). Then it sets the offset based on the IP header's IHL field (right-shifting by 6 eliminates the offset of the field and at the same time converts the value into byte unit). Finally, using link, the hash table from the first call is referenced, which holds the filter from the second call.
๐ฆ SEE ALSO ๐ฆ
tc(8)cls_u32.txtat http://linux-tc-notes.sourceforge.net/
Bestie, that's the whole page ๐ซถ iproute2, dated 25 Sep 201โฆ (the end of that date got cut off, so no cap, I'm not guessing ๐ญ) ๐๐
๐ฆ where this page comes from
- alpine 3.22: iproute2-doc 6.15.0-r0 ยท GPL-2.0-or-later
- alpine 3.23: iproute2-doc 6.17.0-r0 ยท GPL-2.0-or-later
- alpine 3.24: iproute2-doc 7.0.0-r0 ยท GPL-2.0-or-later
- arch rolling: iproute2 7.2.0-1
- arch rolling: iproute2 7.3.0-1 ยท GPL-2.0-or-later
- centos-stream 10: iproute-tc 7.0.0-2.el10 ยท GPL-2.0-or-later
- centos-stream 9: iproute-tc 6.17.0-3.el9 ยท GPL-2.0-or-later
- debian bookworm: iproute2 6.1.0-3 ยท GPL-2.0-only
- debian trixie: iproute2 6.15.0-1 ยท GPL-2.0-only
- fedora 43: iproute-tc 6.14.0-2.fc43 ยท GPL-2.0-or-later
- fedora 44: iproute-tc 6.17.0-2.fc44 ยท GPL-2.0-or-later
- nixos 26.05: iproute2 7.0.0
- nixpkgs unstable: iproute2 7.2.0
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- ubuntu noble: iproute2 6.1.0-1ubuntu6 ยท GPL-2.0-only
- ubuntu noble: iproute2 6.1.0-1ubuntu6.4 ยท GPL-2.0-only
- ubuntu questing: iproute2 6.16.0-1ubuntu2
- ubuntu questing: iproute2 6.16.0-1ubuntu2.1 ยท GPL-2.0-only
- ubuntu questing: iproute2 6.16.0-1ubuntu2.2
- ubuntu resolute: iproute2 6.19.0-1ubuntu1 ยท GPL-2.0-only
- ubuntu resolute: iproute2 6.19.0-1ubuntu1.1 ยท GPL-2.0-only
- ubuntu stonking: iproute2 7.2.0-1ubuntu1 ยท GPL-2.0-only
- ๐ girlified by anthropic/claude-haiku-5.5 on : a language model's rewrite of the original, under its terms