TC-U32(8)

๐Ÿ“ฆ
version 7.3.0 ยท 2 versions
  • 7.3.0 ยท alpine 3.22, alpine 3.23, alpine 3.24, arch rolling, centos-stream 10, centos-stream 9, debian bookworm, debian trixie, fedora 43, fedora 44, nixos 26.05, nixpkgs unstable, opensuse-leap 16.0, ubuntu noble, ubuntu questing, ubuntu resolute, ubuntu stonking (latest)
  • 5.15.0 ยท ubuntu jammy
, in alpine 3.22, alpine 3.23, alpine 3.24, arch rolling, centos-stream 10, centos-stream 9, debian bookworm, debian trixie, fedora 43, fedora 44, nixos 26.05, nixpkgs unstable, opensuse-leap 16.0, ubuntu noble, ubuntu questing, ubuntu resolute, ubuntu stonking

๐Ÿ’– 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:

  1. Create a new hash table, specifying its size using the divisor parameter 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 ๐Ÿ™ˆ.
  2. Create filters that link to the table from step 1 using the link parameter, and define the packet data the kernel will use to calculate the hashkey.
  3. 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 sample parameter. 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, u16 and u8 prefixes mean four, two and single byte unsigned values. E.g. u16 is a two byte-sized value in the range between 0 and 65535 (0xFFFF) inclusive ๐Ÿ’….
  • int prefix 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 0x are hexadecimal, a leading 0 means octal, and everything else is decimal ๐Ÿงš.

There are some values with special formatting too ๐Ÿซถ:

  • ip_address and netmask use dotted-quad formatting, as usual for IPv4 addresses ๐ŸŒˆ.
  • An ip6_address is written in common, colon-separated hexadecimal format ๐Ÿ’œ.
  • prefixlen is 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 against ADDR ๐Ÿ’–. The reserved words default, any and all effectively 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 are tos and precedence.
  • 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_value has to be 6 ๐Ÿค“.
  • protocol VAL_MASK_8 โ€” Matches the Protocol (IPv4) or Next Header (IPv6) field value, e.g. 6 for TCP.
  • icmp_type VAL_MASK_8, icmp_code VAL_MASK_8 โ€” Assumes a next-header protocol of icmp or ipv6-icmp and 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 like tun or ppp.

โœจ 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 ๐Ÿฆ‹


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