TC-BPF(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-BPF(8) โœจ

TC-BPF(8) is the Linux manual page for BPF classifier and actions in tc(8), and it's the page for all things BPF inside tc. Let's get into it ๐Ÿฆ‹

๐ŸŒธ NAME ๐ŸŒธ

BPF, bestie ๐Ÿ’– It's a BPF programmable classifier and actions for ingress/egress queueing disciplines. Basically you write your own little traffic-sorting brain and plug it into the ingress and egress qdiscs. Iconic.

๐Ÿงš SYNOPSIS ๐Ÿงš

๐Ÿ’… eBPF classifier (filter) or action:

tc filter ... bpf [ object-file OBJ_FILE ] [ section CLS_NAME ] [ export UDS_FILE ] [ verbose ] [ direct-action | da ] [ skip_hw | skip_sw ] [ police POLICE_SPEC ] [ action ACTION_SPEC ] [ classid CLASSID ]
tc action ... bpf [ object-file OBJ_FILE ] [ section CLS_NAME ] [ export UDS_FILE ] [ verbose ]

๐ŸŽ€ cBPF classifier (filter) or action:

tc filter ... bpf [ bytecode-file BPF_FILE | bytecode BPF_BYTECODE ] [ police POLICE_SPEC ] [ action ACTION_SPEC ] [ classid CLASSID ]
tc action ... bpf [ bytecode-file BPF_FILE | bytecode BPF_BYTECODE ]

๐Ÿ“– DESCRIPTION ๐Ÿ“–

okay so, Extended Berkeley Packet Filter (eBPF) and classic Berkeley Packet Filter are both available as a fully programmable and highly efficient classifier and actions ๐Ÿ”ฅ The classic one was originally just called BPF, but to keep things clear this page calls it cBPF. Both give you a tiny instruction set for small programs that can be loaded safely into the kernel and run inside a tiny virtual machine in kernel space. An in-kernel verifier makes sure a program always terminates, and that it neither crashes nor leaks data from the kernel. Not a single drama ๐Ÿ˜Œ

In Linux, it's generally considered that eBPF is the successor of cBPF. The kernel internally transforms cBPF expressions into eBPF expressions and then executes the eBPF ones. Execution can happen in an interpreter, or at setup time the code can be just-in-time compiled (JIT'ed) to run as native machine code โšก

Right now, the eBPF JIT compiler is available for these architectures:

  • x86_64 (since Linux 3.18)
  • arm64 (since Linux 3.18)
  • s390 (since Linux 4.1)
  • ppc64 (since Linux 4.8)
  • sparc64 (since Linux 4.12)
  • mips64 (since Linux 4.13)
  • arm32 (since Linux 4.14)
  • x86_32 (since Linux 4.18)

And these ones have cBPF but have not (yet) switched to eBPF JIT support ๐Ÿฅฒ

  • ppc32
  • sparc32
  • mips32

eBPF's instruction set follows similar principles to cBPF's, but it's modelled much closer to the underlying architecture so it can mimic native instruction sets and run faster. It's designed to be JIT'ed with a one to one mapping, which also opens the door for compilers to generate optimized eBPF code through an eBPF backend that runs almost as fast as natively compiled code. Since LLVM provides such an eBPF backend, eBPF programs can be written in a subset of the C language, which is so convenient ๐Ÿซถ eBPF infrastructure also comes with a construct called "maps". eBPF maps are key/value stores that are shared between multiple eBPF programs, and also between eBPF programs and user space applications.

For the traffic control subsystem, classifiers and actions that can be attached to ingress and egress qdiscs can be written in eBPF or cBPF. The big advantage over other classifiers and actions is that eBPF/cBPF gives you the generic framework, and you get to implement your highly specialized use cases efficiently. So the classifier or action you write this way won't suffer from feature bloat, and it can execute its task super efficiently. It allows for non-linear classification and even merging the action part into the classification. Combined with efficient eBPF map data structures, user space can push new policies like classids into the kernel without reloading a classifier. It can also gather statistics that get pushed into one map while another map is used for dynamically load balancing traffic based on the determined load. Just a few examples, but you get the vibe ๐ŸŒˆ

๐ŸŽ€ PARAMETERS ๐ŸŽ€

๐Ÿงฉ object-file

  • object-file โ€” this points to an object file in the Executable and Linkable Format (ELF) that contains eBPF opcodes and eBPF map definitions. The LLVM compiler infrastructure, with clang(1) as its C language front end, is one project that can emit eBPF object files you can pass to the eBPF classifier (more details in the EXAMPLES section). This option is mandatory when you want to load an eBPF classifier or action ๐Ÿšจ

๐Ÿงฉ section

  • section โ€” the name of the ELF section in the object file where your eBPF classifier or action lives. By default the section name for the classifier is "classifier", and for the action it's "action". A single object file can hold multiple classifiers and actions, so if yours uses a different name than the defaults, you need to specify it here ๐Ÿท๏ธ

๐Ÿงฉ export

  • export โ€” this points to a Unix domain socket file. If the eBPF object file also has a section named "maps" with eBPF map specifications, the map file descriptors can be handed off through the Unix domain socket to an eBPF "agent". That agent holds on to all the descriptors after tc is done with them. It can be some third party app that implements the IPC counterpart for the import, and it uses them to call into the bpf(2) system call to read out or update eBPF map data from user space. Think monitoring, or pushing down new policies ๐Ÿ‘€

๐Ÿงฉ verbose

  • verbose โ€” if set, it dumps the eBPF verifier output even when loading the eBPF program worked fine. By default, the verifier log only gets emitted to you when something goes wrong ๐Ÿ™ˆ

๐Ÿงฉ direct-action | da

  • direct-action, da โ€” tells the eBPF classifier to not invoke external TC actions. Instead, it uses the TC action return codes (TC_ACT_OK, TC_ACT_SHOT etc.) for classifiers. Main character energy ๐Ÿ‘‘

๐Ÿงฉ skip_hw | skip_sw

  • skip_hw, skip_sw โ€” these are hardware offload control flags. By default TC will try to offload filters to hardware when that's possible. skip_hw explicitly disables the attempt to offload. skip_sw forces the offload and disables running the eBPF program in the kernel. If hardware offload isn't possible and this flag was set, the kernel reports an error and the filter won't be installed at all ๐Ÿ˜ญ

๐Ÿงฉ police

  • police โ€” an optional parameter for an eBPF/cBPF classifier. It specifies a police in tc(1) that gets attached to the classifier, for example on an ingress qdisc ๐Ÿš“

๐Ÿงฉ action

  • action โ€” also an optional parameter for an eBPF/cBPF classifier. It specifies a subsequent action in tc(1) that gets attached to a classifier โžก๏ธ

๐Ÿงฉ classid | flowid

  • classid, flowid โ€” this gives the default traffic control class identifier for this eBPF/cBPF classifier. The default can also be overwritten by the return code of the eBPF/cBPF program. A default return code of -1 means the class identifier you provided here gets used. A return code of 0 from the eBPF/cBPF program implies that no match took place, and any return code other than those two overrides the default classid. This gives you efficient, non-linear classification with a single eBPF/cBPF program, instead of multiple individual programs for various class identifiers that would each need to reparse the packet contents ๐Ÿง 

๐Ÿงฉ bytecode

  • bytecode โ€” only used for loading cBPF classifiers and actions. The cBPF bytecode gets passed directly as a text string in the form 's,c t f k,c t f k,c t f k,...', where s is the number of subsequent 4-tuples. Each 4-tuple consists of the decimals c t f k: c is the cBPF opcode, t is the jump true offset target, f is the jump false offset target, and k is the immediate constant/literal. There are various tools that generate code in this loadable format, for example bpf_asm, which ships with the Linux kernel source tree under tools/net/. So it's definitely not expected that you hack this by hand ๐Ÿ™…โ€โ™€๏ธ The bytecode or bytecode-file option is mandatory when a cBPF classifier or action is to be loaded.

๐Ÿงฉ bytecode-file

  • bytecode-file โ€” also used to load a cBPF classifier or action. It's basically the same as bytecode, except the cBPF bytecode isn't passed straight through the command line. It lives in a text file instead ๐Ÿ“„

๐Ÿ“ EXAMPLES ๐Ÿ“

๐Ÿ› ๏ธ eBPF TOOLING

A full blown example, including eBPF agent code, can be found inside the iproute2 source package under examples/bpf/ ๐ŸŽ

As prerequisites, the kernel needs to have the eBPF system call, namely bpf(2), enabled. It also ships with the cls_bpf and act_bpf kernel modules for the traffic control subsystem. To enable eBPF/eBPF JIT support, depending on which of the two the given architecture supports:

echo 1 > /proc/sys/net/core/bpf_jit_enable

A given restricted C file can be compiled via LLVM like this:

clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o

Okay so bestie, we're picking up right where the examples get spicy ๐Ÿ’…

Adding an eBPF classifier from an object file is trivial if the classifier sits in the default ELF section ๐Ÿฆ‹ (quick heads-up: you can also write obj instead of object-file, because shortcuts slay โœจ):

bcc bpf.c
tc filter add dev em1 parent 1: bpf obj bpf.o flowid 1:1

If your classifier lives in ELF section mycls, you run the same command but add sec mycls ๐ŸŽ€:

tc filter add dev em1 parent 1: bpf obj bpf.o sec mycls flowid 1:1

Dumping the config tells you where the classifier came from. Here it's from object file bpf.o under section mycls, which is so helpful when you've got a dozen filters floating around ๐Ÿ™ˆ:

tc filter show dev em1
filter parent 1: protocol all pref 49152 bpf
filter parent 1: protocol all pref 49152 bpf handle 0x1 flowid 1:1
bpf.o:[mycls]

Same program, but now on the ingress qdisc instead of egress ๐ŸŒธ:

tc qdisc add dev em1 handle ffff: ingress
tc filter add dev em1 parent ffff: bpf obj bpf.o sec mycls flowid
ffff:1

And then you dump it from there again โœจ:

tc filter show dev em1 parent ffff:
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 flowid ffff:1
bpf.o:[mycls]
Heads up, ingress has a limit ๐Ÿ˜ญ It doesn't have an actual underlying queueing discipline. What it can do is classify, mangle, redirect, or drop packets. If you need queueing on the ingress side, ingress has to redirect packets to the ifb device. Otherwise you can use policing ๐Ÿซถ. Ingress is also great for an early drop point for unwanted packets before they reach the upper layers of the networking stack, for network accounting with eBPF maps that can be shared with egress, or as an early mangle and/or redirection point to other networking devices ๐Ÿ’œ.

You can stuff multiple eBPF actions and classifiers into one object file, spread across different sections. When you do that, the non-default section names have to be given explicitly, and in this example that applies to both actions ๐Ÿ‘‘:

tc filter add dev em1 parent 1: bpf obj bpf.o flowid 1:1 \
                         action bpf obj bpf.o sec action-mark \
                         action bpf obj bpf.o sec action-rand ok

The payoff is that the classifier and both actions can share eBPF maps with each other, as long as the programs are written to do that โญ.

Now here's the fun part. If you want to touch eBPF maps from user space beyond the lifetime of the tc(8) setup, you can hand ownership over to an eBPF agent through Unix domain sockets ๐Ÿงš. There are two ways to pull this off:

  1. Write your own eBPF agent. It sets up the Unix domain socket and speaks the protocol that tc(8) dictates. You can find a code example in the iproute2 source package under examples/bpf/ ๐Ÿ’–.
  2. Use tc exec to pass the eBPF map file descriptors through a Unix domain socket and spawn an application such as sh(1) ๐ŸŒˆ. The big perk here: tc places the file descriptors into the environment, so they're available just like the stdin, stdout, and stderr file descriptors. That means if your user applications run inside this fd-owner shell, they can die and restart without losing their eBPF map file descriptors. Here's an example using the classifier and action mix from before:
tc exec bpf imp /tmp/bpf
tc filter add dev em1 parent 1: bpf obj bpf.o exp /tmp/bpf flowid
1:1 \
                         action bpf obj bpf.o sec action-mark \
                         action bpf obj bpf.o sec action-rand ok

Assuming the maps are shared between the classifier and the actions, you only have to export them once, for example from inside the classifier or action command. tc sets up all the eBPF map file descriptors the first time the object file gets parsed ๐Ÿ“.

When a shell gets spawned, the environment picks up a few eBPF related variables ๐Ÿ”ฅ:

  • BPF_NUM_MAPS โ€” the total number of maps transferred over the Unix domain socket.
  • BPF_MAP<X> โ€” its value is the file descriptor number you can access from your eBPF agent. You can use it directly as the file descriptor for the bpf(2) system call to read or change eBPF map values. <X> is the identifier of the eBPF map, and it matches the id member of struct bpf_elf_map from the tc eBPF map specification.

Here's what the environment looks like in this example ๐Ÿงš:

sh# env | grep BPF
    BPF_NUM_MAPS=3
    BPF_MAP1=6
    BPF_MAP0=5
    BPF_MAP2=7
sh# ls -la /proc/self/fd
    [...]
    lrwx------. 1 root root 64 Apr 14 16:46 5 -> anon_inode:bpf-map
    lrwx------. 1 root root 64 Apr 14 16:46 6 -> anon_inode:bpf-map
    lrwx------. 1 root root 64 Apr 14 16:46 7 -> anon_inode:bpf-map
sh# my_bpf_agent

eBPF agents are seriously useful ๐Ÿ’œ. They can prepopulate eBPF maps from user space, monitor statistics through maps, and then use that feedback to, say, rewrite classids in eBPF map values while things are running. Since agents are just normal applications, they can also receive traffic control policies from external controllers and push them down into eBPF maps so they adapt to network conditions on the fly. And eBPF maps can be shared with other eBPF program types too (tracing, for example), so you can build some really powerful combos ๐Ÿ”ฅ.


โœจ eBPF PROGRAMMING โœจ

eBPF classifiers and actions are written in restricted C syntax ๐Ÿ’…. In the future there could be new language frontends too, so keep an eye out ๐Ÿ‘€.

The header linux/bpf.h gives you eBPF helper functions you can call from an eBPF program. This man page only has two minimal, stand-alone examples. If you want to see the possibilities in action, check out the fully fledged flow dissector example in examples/bpf from the iproute2 source package ๐Ÿฆ‹.

Here are the supported 32 bit classifier return codes from the C program and what they mean ๐ŸŽ€:

  • 0 โ€” a mismatch.
  • -1 โ€” the default classid configured from the command line.
  • anything else โ€” overrides the default classid, which gives you a facility for non-linear matching โœจ.

Here are the supported 32 bit action return codes from the C program and what they mean (from linux/pkt_cls.h) ๐Ÿ’…:

  • TC_ACT_OK (0) โ€” terminates the packet processing pipeline and lets the packet proceed ๐Ÿซถ.
  • TC_ACT_SHOT (2) โ€” terminates the packet processing pipeline and drops the packet ๐Ÿ’€.
  • TC_ACT_UNSPEC (-1) โ€” uses the default action configured from tc (same as returning -1 from a classifier) ๐Ÿ™ˆ.
  • TC_ACT_PIPE (3) โ€” moves on to the next action, if there is one ๐ŸŒธ.
  • TC_ACT_RECLASSIFY (1) โ€” terminates the packet processing pipeline and starts classification from the beginning ๐Ÿ”.
  • anything else โ€” an unspecified return code, so no promises there ๐Ÿ˜ญ.

Both classifier and action return codes are supported in eBPF and cBPF programs ๐ŸŒˆ.

To show off that restricted C syntax, here's a minimal toy classifier. It assumes egress packets (say, ones coming from a container) have already been marked in the interval [0, 255]. The program keeps stats on each mark for user space, and it maps the classid to the root qdisc, using the mark itself as the minor handle ๐Ÿ’–:

#include <stdint.h>
#include <asm/types.h>

#include <linux/bpf.h>
#include <linux/pkt_sched.h>

#include "helpers.h"

struct tuple {
        long packets;
        long bytes;
};

#define BPF_MAP_ID_STATS        1 /* agent's map identifier */
#define BPF_MAX_MARK            256

struct bpf_elf_map __section("maps") map_stats = {
        .type           =       BPF_MAP_TYPE_ARRAY,
        .id             =       BPF_MAP_ID_STATS,
        .size_key       =       sizeof(uint32_t),
        .size_value     =       sizeof(struct tuple),
        .max_elem       =       BPF_MAX_MARK,
        .pinning        =       PIN_GLOBAL_NS,
};

static inline void cls_update_stats(const struct __sk_buff *skb,
                                    uint32_t mark)
{
        struct tuple *tu;

        tu = bpf_map_lookup_elem(&map_stats, &mark);
        if (likely(tu)) {
                __sync_fetch_and_add(&tu->packets, 1);
                __sync_fetch_and_add(&tu->bytes, skb->len);
        }
}

__section("cls") int cls_main(struct __sk_buff *skb)
{
        uint32_t mark = skb->mark;

        if (unlikely(mark >= BPF_MAX_MARK))
                return 0;

        cls_update_stats(skb, mark);

        return TC_H_MAKE(TC_H_ROOT, mark);
}

char __license[] __section("license") = "GPL";

Here's another small one: a port redirector ๐Ÿงš. It demuxes destination port 80 into the interval [8080, 8087], steered by RSS, and it can be attached to the ingress qdisc. Adding the egress counterpart and IPv6 support is left as an exercise for the reader, bestie ๐Ÿ˜ค:

#include <asm/types.h>
#include <asm/byteorder.h>

#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

#include "helpers.h"

static inline void set_tcp_dport(struct __sk_buff *skb, int nh_off,
                                 __u16 old_port, __u16 new_port)
{
        bpf_l4_csum_replace(skb, nh_off + offsetof(struct tcphdr, check),
                            old_port, new_port, sizeof(new_port));
        bpf_skb_store_bytes(skb, nh_off + offsetof(struct tcphdr, dest),
                            &new_port, sizeof(new_port), 0);
}

static inline int lb_do_ipv4(struct __sk_buff *skb, int nh_off)
{
        __u16 dport, dport_new = 8080, off;
        __u8 ip_proto, ip_vl;

        ip_proto = load_byte(skb, nh_off +
                             offsetof(struct iphdr, protocol));
        if (ip_proto != IPPROTO_TCP)
                return 0;

        ip_vl = load_byte(skb, nh_off);
        if (likely(ip_vl == 0x45))
                nh_off += sizeof(struct iphdr);
        else
                nh_off += (ip_vl & 0xF) << 2;

        dport = load_half(skb, nh_off + offsetof(struct tcphdr, dest));
        if (dport != 80)
                return 0;
                off = skb->queue_mapping & 7;
                set_tcp_dport(skb, nh_off - BPF_LL_OFF, __constant_htons(80),
                              __cpu_to_be16(dport_new + off));
                return -1;
        }

        __section("lb") int lb_main(struct __sk_buff *skb)
        {
                int ret = 0, nh_off = BPF_LL_OFF + ETH_HLEN;

                if (likely(skb->protocol == __constant_htons(ETH_P_IP)))
                        ret = lb_do_ipv4(skb, nh_off);

                return ret;
        }

        char __license[] __section("license") = "GPL";

Okay and now the helper header that both of those examples lean on is helpers.h, and it looks like this ๐ŸŽ€

/* Misc helper macros. */
#define __section(x) __attribute__((section(x), used))
#define offsetof(x, y) __builtin_offsetof(x, y)
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

/* Object pinning settings */
#define PIN_NONE       0
#define PIN_OBJECT_NS  1
#define PIN_GLOBAL_NS  2

/* ELF map definition */
struct bpf_elf_map {
    __u32 type;
    __u32 size_key;
    __u32 size_value;
    __u32 max_elem;
    __u32 flags;
    __u32 id;
    __u32 pinning;
    __u32 inner_id;
    __u32 inner_idx;
};

/* Some used BPF function calls. */
static int (*bpf_skb_store_bytes)(void *ctx, int off, void *from,
                                  int len, int flags) =
      (void *) BPF_FUNC_skb_store_bytes;
static int (*bpf_l4_csum_replace)(void *ctx, int off, int from,
                                  int to, int flags) =
      (void *) BPF_FUNC_l4_csum_replace;
static void *(*bpf_map_lookup_elem)(void *map, void *key) =
      (void *) BPF_FUNC_map_lookup_elem;

/* Some used BPF intrinsics. */
unsigned long long load_byte(void *skb, unsigned long long off)
    asm ("llvm.bpf.load.byte");
unsigned long long load_half(void *skb, unsigned long long off)
    asm ("llvm.bpf.load.half");

Best practice time, bestie ๐Ÿ’…: we strongly recommend only having a single eBPF classifier loaded in tc ๐Ÿฆ‹ and doing all your matching and mangling from inside that one program, instead of stacking up a whole list of individual classifiers and separate actions ๐Ÿ™ˆ. One classifier tailored to your exact use-case is the most efficient thing to run โœจ.


๐Ÿ› eBPF DEBUGGING ๐Ÿ›

okay so both tc filter and tc action commands for bpf accept an optional verbose parameter ๐Ÿ”, which lets you peek at the eBPF verifier log. Lowkey the best part: it gets dumped by default whenever something goes wrong ๐Ÿ˜ญ.

If the eBPF/cBPF JIT compiler is turned on, you can also tell it to emit a debug output of the resulting opcode image into the kernel log ๐Ÿ“œ, which you can read with dmesg(1) ๐Ÿ’–:

echo 2 > /proc/sys/net/core/bpf_jit_enable

The Linux kernel source tree also ships a little helper under tools/net/ called bpf_jit_disasm ๐Ÿงš. It reads that opcode image dump out of the kernel log and spits back the disassembly โœจ:

bpf_jit_disasm -o

And then there's the Linux kernel's extensive eBPF/cBPF test suite module called test_bpf ๐ŸŒˆ. Upon ...

modprobe test_bpf

... it runs a whole buffet of test cases and dumps the results into the kernel log, which you can inspect with dmesg(1) ๐Ÿ‘‘. The results can differ depending on whether the JIT compiler is enabled or not ๐Ÿคทโ€โ™€๏ธ. If any test cases fail, the module will fail to load ๐Ÿ’€. In that case, we urge you to file a bug report to the related JIT authors, Linux kernel and networking mailing lists ๐Ÿซถ.


๐ŸŒธ cBPF ๐ŸŒธ

Real talk: we generally recommend switching to implementing eBPF classifiers and actions ๐Ÿ’œ. But for the sake of completeness, a few words on how to program in cBPF will be lost here ๐Ÿ˜ญ.

Likewise, the bpf_jit_enable switch can be enabled as mentioned already โœจ. And tooling like bpf_jit_disasm doesn't care whether eBPF or cBPF code is being loaded ๐Ÿ™Œ.

Unlike eBPF, classifiers and actions are not written in restricted C ๐Ÿ™…โ€โ™€๏ธ. They're written in a minimal assembler-like language, or with the help of other tooling ๐Ÿ› ๏ธ.

The raw interface with tc takes opcodes directly ๐Ÿค“. For example, the most minimal classifier, matching on every packet and resulting in the default classid of 1:1, looks like this ๐Ÿ’–:

tc filter add dev em1 parent 1: bpf bytecode '1,6 0 0 4294967295,' flowid 1:1

The first decimal of the bytecode sequence is the number of subsequent 4-tuples of cBPF opcodes ๐Ÿ”ข. Each 4-tuple is made of c t f k decimals, where c is the cBPF opcode, t is the jump true offset target, f is the jump false offset target, and k is the immediate constant/literal ๐Ÿ“. This particular one is an unconditional return from the program with an immediate value of -1 โœจ.

So, for egress classification, Willem de Bruijn implemented a minimal stand-alone helper tool ๐Ÿ‘‘ under the GNU General Public License version 2 for the iptables(8) BPF extension. It basically abuses the libpcap internal classic BPF compiler ๐Ÿง . His code, derived here for usage with tc(8), looks like this:

#include <pcap.h>
#include <stdio.h>

int main(int argc, char **argv)
{
        struct bpf_program prog;
        struct bpf_insn *ins;
        int i, ret, dlt = DLT_RAW;

        if (argc < 2 || argc > 3)
                return 1;
        if (argc == 3) {
                dlt = pcap_datalink_name_to_val(argv[1]);
                if (dlt == -1)
                        return 1;
        }

        ret = pcap_compile_nopcap(-1, dlt, &prog, argv[argc - 1],
                                  1, PCAP_NETMASK_UNKNOWN);
        if (ret)
                return 1;

        printf("%d,", prog.bf_len);
        ins = prog.bf_insns;

        for (i = 0; i < prog.bf_len - 1; ++ins, ++i)
                printf("%u %u %u %u,", ins->code,
                       ins->jt, ins->jf, ins->k);
        printf("%u %u %u %u",
               ins->code, ins->jt, ins->jf, ins->k);

        pcap_freecode(&prog);
        return 0;
}

With this small helper, any tcpdump(8) filter expression can be abused as a classifier, where a match results in the default classid ๐ŸŒˆ:

bpftool EN10MB 'tcp[tcpflags] & tcp-syn != 0' > /var/bpf/tcp-syn
tc filter add dev em1 parent 1: bpf bytecode-file /var/bpf/tcp-syn flowid 1:1

Basically, such a minimal generator is equivalent to this ๐Ÿงš:

tcpdump -iem1 -ddd 'tcp[tcpflags] & tcp-syn != 0' | tr '\n' ',' > /var/bpf/tcp-syn
Heads up ๐Ÿ™ˆ: libpcap doesn't support all of Linux's specific cBPF extensions in its compiler. So the Linux kernel also ships a minimal BPF assembler called bpf_asm under tools/net/ for full control ๐Ÿ’…. For detailed syntax and semantics on writing these programs by hand, see the references under FURTHER READING.

Here's a trivial toy bpf_asm example for classifying IPv4/TCP packets, saved in a text file called foobar ๐Ÿฆ‹:

ldh [12]
jne #0x800, drop
ldb [23]
jneq #6, drop
ret #-1
drop: ret #0

And you can load a classifier like that with:

bpf_asm foobar > /var/bpf/tcp-syn
tc filter add dev em1 parent 1: bpf bytecode-file /var/bpf/tcp-syn flowid 1:1

For BPF classifiers, the Linux kernel additionally provides a small BPF debugger under tools/net/ called bpf_dbg ๐Ÿ”ฅ. It can be used to test a classifier against pcap files, single-step through it, or add various breakpoints into the classifier program and dump register contents during runtime ๐Ÿ”.

Implementing an action in classic BPF is rather limited, because packet mangling is not supported ๐Ÿ˜ญ. That's why it's generally recommended to make the switch to eBPF whenever possible ๐Ÿ’–.


๐Ÿ“š FURTHER READING ๐Ÿ“š

Further and more technical details about the BPF architecture can be found in the Linux kernel source tree under Documentation/networking/filter.txt ๐Ÿค“.

Further details on eBPF tc(8) examples can be found in the iproute2 source tree under examples/bpf/ ๐ŸŒธ.

๐Ÿ”— SEE ALSO ๐Ÿ”—

tc(8), tc-ematch(8), bpf(2), bpf(4) ๐Ÿ’œ

๐ŸŽ€ AUTHORS ๐ŸŽ€

Manpage written by Daniel Borkmann ๐Ÿ‘‘.

Please report corrections or improvements to the Linux kernel networking mailing list: <netdev@vger.kernel.org> ๐Ÿซถ.


Okay bestie, that's the whole thing โœจ๐Ÿ’… The sign-off is from iproute2, last touched 18 May 201 ๐ŸŒˆ (the date got cut off, so you know the vibe ๐Ÿ™ˆ). Stay iconic, and go write some eBPF ๐Ÿฆ‹๐Ÿ’–

๐Ÿ“ฆ where this page comes from