1 of 64

Understanding eBPF & XDP

Deep Dive into Kernel Architecture, Packet Processing & High-Performance Networking

SUSHANTH SATHESH RAO

2 of 64

Why eBPF?

Problems before eBPF:

  • Kernel modules were unsafe (could crash system)
  • Debugging modules was extremely difficult
  • No dynamic tracing
  • No easy way to inject logic inside running kernel
  • Requiring kernel recompilation for new features
  • Cross kernel compatibility issues
  • Security restrictions

eBPF Solution:

  • Safe, verifiable, dynamic kernel extension
  • No kernel recompilation
  • No modules needed
  • Attach to many kernel events
  • Observability + control + security in one framework
  • Compatible across every kernel version

3 of 64

eBPF - Extended Berkley Packet Filter

eBPF is an in-kernel virtual machine that executes user-supplied programs safely, verifiably, and efficiently at specific kernel hook points.

Key properties:

  • Verifier guarantees safety
  • JIT-compiled for speed
  • Runs inside kernel with controlled capabilities
  • Event-driven: triggered by syscalls, packets,�kprobes, timers…
  • Uses maps for persistent kernel/user data sharing

4 of 64

5 of 64

eBPF Program Lifecycle (Step-by-Step)

  1. Write C program
  2. Compile using Clang → eBPF bytecode
  3. Load via syscall
  4. Verifier inspects program
  5. Program is either rejected or accepted
  6. JIT compiler compiles the accepted program
  7. Program is attached to hook
  8. Kernel invokes it on events and runs it in the�sandboxed VM

6 of 64

7 of 64

Verifier: Why It Is Critical

Verifier enforces:

  • No invalid pointer access
  • No out-of-bounds reads/writes
  • No unbounded loops
  • All paths must be safe
  • Register type tracking
  • Stack analysis
  • Map access validation
  • Helper argument validation

8 of 64

Verifier Internal Model

Verifier performs:

  • Control Flow Graph (CFG) Construction
    • Ensures no unreachable code
    • Checks jump validity
  • Symbolic Execution
    • Simulates registers and stack on all paths
    • Tracks pointer types, ranges
    • Tracks packet boundaries
    • Ensures loop termination
    • Ensures helper arguments are valid
  • State Explosion Control
    • Prunes equivalent states
    • Limits branching

9 of 64

eBPF Registers, Stack, and Memory Model

  • 11 registers: R0–R10
  • R0 is return value
  • R1–R5 are argument registers
  • R6–R9 are callee-saved
  • R10 is frame pointer (stack base)
  • 512-byte stack limit
  • No heap allocation
  • Memory access restricted by verifier

10 of 64

11 of 64

12 of 64

BPF Instruction Set

13 of 64

eBPF MAPS

eBPF maps are key-value data structures residing in the Linux kernel that enable communication and data sharing between eBPF programs, and between eBPF programs and user-space applications.

  • Shared Storage: Maps provide a shared memory space within the kernel, allowing different eBPF programs, or an eBPF program and a user-space application, to access and modify the same data.
  • Various Data Structures: eBPF maps support a range of data structures, including hash tables, arrays, ring buffers, and others. The specific map type dictates its behavior and how data is stored and retrieved.
  • Kernel-Space Residency: Maps are created and managed within the kernel, ensuring efficient access for eBPF programs running in the kernel's context.
  • User-Space Interaction: User-space applications can interact with eBPF maps using the bpf() system call, allowing them to create, update, delete, and lookup elements within a map. This enables user-space to configure eBPF programs or retrieve data collected by them.
  • State Management: eBPF programs, being stateless by design, rely on maps to store and retrieve state information across different invocations or for long-term data persistence.
  • Communication Channel: Maps act as a conduit for passing information and metrics from the kernel to user-space, facilitating observability, monitoring, and security solutions built on eBPF.

14 of 64

15 of 64

Types of maps

General-purpose maps (common to all subsystems):

  • Hash / Array / Per-CPU Hash / Per-CPU Array
  • LRU Hash Maps
  • LPM Trie
  • Ring Buffer
  • Queue / Stack
  • Perf Event Array

Networking-specific maps:

  • devmap: redirect packets to interfaces
  • cpumap: send packets to specific CPUs
  • xskmap: AF_XDP user-space zero-copy sockets
  • sockmap / sockhash: attach programs to sockets

16 of 64

Linux Kernel Network Stack

OSI Layer

Linux Kernel

Kernel Source Paths

Key Files and/or Functions

What the code does

Layer 1 — Physical

NIC hardware drivers

drivers/net/ and per-vendor dirs like drivers/net/ethernet/intel/, drivers/net/ethernet/mellanox/

- Driver probe code

- DMA ring setup

- ndo_open, ndo_start_xmit implementations

- IRQ handlers

- NAPI polling functions

- Hardware initialization

- DMA buffer management

- Interrupt moderation

- RX/TX descriptor handling

- Integrates with NAPI framework

Layer 2 — Data Link (Ethernet, ARP, VLAN)

Networking device layer + Ethernet stack

net/core/, net/ethernet/, net/802/

- dev.c (core net_device logic)

- eth.c (Ethernet helpers)

- arp.c (ARP implementation)

- vlan/ (802.1Q/802.1ad)

- link_watch.c

- net_device struct & registration

- Link state changes

- ARP protocol

- MAC address handling

- VLAN tagging/stripping

- Carrier detection

17 of 64

Linux Kernel Network Stack (Contd.)

Layer 2.5 — Bridging & Tunneling

Linux bridge, VXLAN, GRE, Geneve, LWT

net/bridge/, net/core/lwtunnel/, net/ipv4/ip_gre.c, net/ipv6/ip6_gre.c, net/vxlan/

- STP/RSTP

- br_forward.c

- VXLAN (udp tunnel driver)

- GRE/IPIP

- Lightweight tunnels

- Software L2 bridging

- Overlay networks

- Encapsulation / decapsulation

Layer 3 — Network Layer (IPv4/IPv6 routing)

IP stack, routing table, netfilter

net/ipv4/, net/ipv6/, net/core/

- ip_input.c, ip_forward.c

- ip_output.c

- route.c, fib_trie.c

- IPv6: ip6_input.c, ip6_output.c, ip6_route.c

- Packet forwarding & routing

- FIB lookup

- PMTU discovery

- ICMPv4/v6

- IP fragmentation / reassembly

Layer 3.5 — Netfilter + XDP/TC ingress

Firewalling, packet mangling, eBPF attach points

net/netfilter/, kernel/bpf/, net/sched/

- Netfilter hooks (nf_hook.c)

- xtables matches/targets

- cls_bpf, act_bpf (TC BPF)

- XDP in drivers

- Firewall filtering

- NAT

- TC ingress classifiers

- XDP pre-processing

18 of 64

Linux Kernel Network Stack (Contd.)

Layer 4 — Transport (TCP/UDP/SCTP)

TCP/UDP protocols, congestion control

net/ipv4/tcp*.c, net/ipv4/udp.c, net/ipv6/tcp*.c, net/ipv6/udp.c, net/sctp/

- tcp_input.c

- tcp_output.c

- tcp_ipv4.c, tcp_ipv6.c

- udp.c

- congestion control modules in net/ipv4/tcp_cong.c

- TCP/UDP processing

- Sockets for transport layer

- Retransmission, RTT estimation

- Congestion control algorithms (CUBIC, BBR, etc.)

Layer 4.5 — Socket Layer

BSD sockets interface, sk_buff, queues

net/core/

- sock.c, skbuff.c

- datagram.c

- stream.c

- Socket API implementation

- SKB allocation/free

- Receive/transmit queues

Layer 5 — Session Layerl

Not explicitly present

N/A

- N/A

- Session-layer protocols (like TLS) live in userspace libraries, not kerne

Layer 6 - Presentation Layer

Not implemented in the kernel

N/A

- N/A

- Encoding/decoding handled in userspace

Layer 7 - Application Layer

Kernel only supports some of them

net/{rpc, dns_resolver, sunrpc}/

- SUNRPC

- NFS client/server modules

- Limited app-layer protocols run in kernel

- Almost all application-layer functionality is user space

19 of 64

Quick Summary

Code

Purpose

net/core/

Central networking framework. SKB, qdisc, GRO/GSO, netdevice core.

net/sched/

Traffic control (TC) qdiscs, classifiers, BPF TC integration.

net/bpf/ & kernel/bpf/

eBPF interpreter, JITs, maps, program types.

drivers/net/

All NIC drivers: Ethernet, tunneling, bonding, wireless, etc.

net/rxrpc

In-kernel RXRPC protocol

net/mac80211/, drivers/net/wireless/

Wi-Fi stack

net/packet/

AF_PACKET socket implementation

20 of 64

High Level Overview

21 of 64

22 of 64

RX Path

23 of 64

RX Path - Physical / Data Link Layer

  • Copies the incoming packet to rx_ring buffer over DMA.
  • Interrupts CPU (handled by driver) - Informing availability of new packet.
    • With NAPI, the interrupt is raised only once, and subsequently the driver enters polling mode, where it polls the ring buffer of the corresponding NIC.
  • The interrupt handler, schedules a software interrupt (NET_RX_IRQ), which runs `net_rx_action`
  • `napi_schedule`, `napi_poll` - Schedule polling of the ring buffer unless already being executed
  • net_rx_action() - This function is run in a dedicated kernel thread (ksoftirqd) or the current CPU's SoftIRQ context. It polls the NIC's ring buffer (via the driver's registered NAPI poll function, e.g., igb_clean_rx_queue) to process packets in bulk, creating an sk_buff structure for each. Quick note: XDP attaches in the driver’s poll function
  • Allocate sk_buff (calls alloc_skb(size, priority))
  • netif_receive_skb(skbuff) function classifies the packet and redirect it to packet handler function. Also a netfilter hook

24 of 64

sk_buff

25 of 64

sk_buff cont*(header)

struct sk_buff{

/* These two members must be first. */

struct sk_buff *next;

struct sk_buff *prev;

struct sock *sk; /* owner socket */

struct skb_timeval tstamp; /* arrival time */

struct net_device *dev; /* output dev */

struct net_device *input_dev; /* input dev */

/* Headers of the above OSI layers */ . . .

. . .

}

26 of 64

RX Path - Network Layer

  1. ip_rcv() receives from physical layer
    1. Performs checks(Length > IP Header (20 bytes), errors, checksum etc )
    2. Discards IP headers
    3. Defragments if required
  2. Pre-Routing net-filter hook
    • iptables(user-space utility) - Configures the net-filter hooks and corresponding actions to be taken
  3. ip_rcv_finish()

27 of 64

RX Path - Transport Layer

  1. tcp_v4_rcv() : is invoked to receive packet from network layer(when it’s TCP)
    1. Checks if packet has valid TCP header - pskb_may_pull()
    2. Initialize checksum and begins with fast path processing
    3. Uses TCP_SKB_CB macro to get tcp control buffer info (tcp parameters)
  2. tcp_prequeue()(Up till v5) : Fast path processing - If there’s a open socket in the user space, then it directly copies data to the user space memory.
  3. tcp_v4_do_rcv() : Handles both fast path & slow path processing
    • If fast path & TCP state is established it invokes tcp_rcv_established() (this also handles slow path packets!)
    • Slow path processing performs various checks & finally sends an ACK(if successful)
    • tcp_data_queue() : Adds data segments into the socket’s receive queue

28 of 64

RX Path - Application Layer

  1. Applications use read, recvfrom, etc to read data
  2. These are finally mapped to tcp_recvmsg()
    1. Copies data from a open socket buffer to user buffer

That completes the receive path

29 of 64

TX Path - Application Layer

  1. write(), send() : Sys call to write data by the user program
    1. sock_sendmsg() : Generates message header and is responsible for handing over the packet to the lower transport layer
    2. inet_sendmsg() : This is invoked for the IP packets which invokes the corresponding transport layer function.

30 of 64

TX Path - Transport Layer

  1. tcp_sendmsg() : Sanity checks and offloading to network layer
    1. Checks if TCP connection is established
    2. skb_copy_to_page()(Linux 3*)/ csum_and_copy_from_user() : Copies the packet from user space to kernel space
    3. tcp_push_one()/tcp_write_xmit(), etc functions are used to invoke the below function depending on the scenario
    4. tcp_transmit_skb() : Build the header, takes care of window scaling, etc
    5. queue_xmit() : Transmits the packet to the network layer

Important data structures:

tcphdr, tcp_skb_cb

31 of 64

TX Path - Network Layer

  1. Route lookup, TTL maintenance.
  2. ip_queue_xmit() : Performs checks and handles routing & forwarding
  3. ip_route_output_flow(): finds a route searching FIB
    1. ip_route_output_slow() - Output route resolving function
  4. ip_fragment(): If fragmenting is required
  5. netfilter POSTROUTING hook is invoked
  6. Once route is resolved dev_queue_xmit() is called to send the packet to the device

Important Data structure:

flowi

32 of 64

TX Path - Data Link Layer

  1. dev_queue_xmit() : Performs device checks
    1. qdisc_restart() : Attempts a packet transmission - Extremely vital component, allows to perform traffic shaping, etc.
    2. hard_start_xmit() : Driver specific implementation which copies the packet to the hardware
    3. On successful transmission, frees up sk_buff
  2. On a failure, the packet is requeued and transmission is attempted in SOFT IRQ context

33 of 64

Queue Disciplines

The Qdisc (Queueing Discipline) controls how packets are queued and scheduled for transmission in Linux.

What a Qdisc is

  • Qdisc is a queuing/scheduling element attached to a network interface.
  • Each interface has one egress Qdisc by default (ingress uses a special pseudo-Qdisc).

It determines:

  • how packets are queued
  • how packets are shaped, delayed, reordered, or dropped
  • which packets get priority

Types of Qdiscs

  • Classful Qdiscs
    • Have children (classes) and can contain other Qdiscs.
    • Examples: htb, hfsc, cbq.
    • Used for hierarchical shaping and per-class bandwidth guarantees.
  • Classless Qdiscs
    • No subclasses, simple queues.
    • Examples: pfifo_fast, fq_codel, fq, pfifo.
    • Used for fairness, low latency, default behavior.

Special Qdiscs

  • mq / multiq — multi-queue NICs
  • ingress — attached to ingress path (pseudo-Qdisc)
  • noqueue — used for devices that don’t queue packets

Where Qdiscs live in the kernel

  • Qdisc core code is in net/sched/sch_generic.c
  • Algorithm-specific Qdiscs in net/sched/sch_*

Key kernel structs:

  • struct Qdisc — main Qdisc object
  • struct Qdisc_ops — operations table
  • struct Qdisc_class_ops — classful Qdisc ops

Role of Qdiscs

  • Queue packets for transmission
  • Shape traffic (policing, rate limiting)
  • Apply flow queuing
  • Enforce fairness
  • Drop or mark packets before TX

34 of 64

Traffic Control Subsystem

TC is the high-level userspace API that configures:

  • Qdiscs
  • Classes
  • Filters
  • Actions

It is configured by the tc command-line utility and interacts with the kernel via netlink.

Core components of TC:

  • Qdiscs
    • Handles queuing.
  • Filters (classifiers)
    • Decide what packets match what rules.
    • Examples:�cls_u32 (multi-level classifier)�cls_flower (match on fields easily)�cls_basic (simple matches)�cls_bpf (match using an eBPF program)
  • Actions
    • Per-packet operations that modify behavior:
      • mirred → redirect / mirror
      • police → rate limit
      • pedit → modify headers
      • Drop
      • bpf → run an eBPF program as an action
  • Chain / block model
    • Allows multiple filters and actions to be chained.
    • eBPF can be attached at any point in these chains.

35 of 64

Hooks in the network stack

  • XDP → Pre-SKB, earliest possible processing
  • TC ingress → Early SKB-based processing
  • TC egress → For shaping and SKB modifications
  • Socket filters → Before user space
  • Cgroup hooks → Per-cgroup routing/shaping
  • kprobes / uprobes → Observability
  • fentry/fexit → Debugging, Observability

36 of 64

37 of 64

38 of 64

TC Ingress

TC ingress is a hook for incoming packets after they have entered the kernel networking stack, before routing.

Key details:

  • It is not a real Qdisc.
  • It is a pseudo-Qdisc always called ingress.
  • Packet path:
    • Driver → NAPI → __netif_receive_skb_core()
    • Before L3 processing, TC ingress filters run.

Capabilities of TC ingress:

  • Early packet filtering
  • Early dropping
  • Mirroring/redirecting (to other interfaces)
  • Running eBPF (cls_bpf or act_bpf)
  • Monitoring traffic

TC ingress limitations:

  • No queueing — packets cannot be delayed.
  • No shaping — shaping only works on egress due to queueing requirement.

Usage examples:

  • Anti-DDoS filtering
  • Early packet classification
  • eBPF-based monitoring or advanced filtering

__netif_receive_skb_core()

→ sch_handle_ingress() → tc_run() → tcf_classify()

→ tc_classify()

→ run filters (cls_bpf, cls_flower, etc.)

39 of 64

TC Egress

TC egress is triggered when packets are being sent out of an interface.

Egress responsibilities:

  • Queue management and scheduling
  • Traffic shaping
  • Prioritization
  • Egress filtering
  • Egress BPF actions

Capabilities:

  • Packet reordering
  • Rate limiting via tokens (HTB, HFSC)
  • Burst control
  • Header mangling
  • Redirection/mirroring
  • eBPF programs for shaping/monitoring

dev_queue_xmit()

→ qdisc_run()

→ qdisc->enqueue()

→ qdisc->dequeue()

→ tc_classify()

40 of 64

TC-BPF

1. cls_bpf (Classifier BPF)

  • Runs during packet classification.
  • Determines whether the packet matches.
  • Returns: TC_ACT_OK, TC_ACT_SHOT (drop), TC_ACT_RECLASSIFY, TC_ACT_PIPE, etc.
  • It does not modify the packet by itself — only classifies.

2. act_bpf (Action BPF)

  • Runs after classification.
  • Can modify packets, redirect, drop, or pass.
  • Capable of:
    • Adjusting packet headers
    • Redirecting to another interface
    • Dropping packets
    • Passing metadata to other TC layers

Where TC-BPF fits in the kernel:

  • Located in net/sched/cls_bpf.c and net/sched/act_bpf.c.
  • Uses eBPF program type BPF_PROG_TYPE_SCHED_CLS.

TC-BPF program behavior:

  • Receives an skb pointer (struct __sk_buff)
  • Can read/write packet data via:
    • Bpf_skb_load_bytes
    • Bpf_skb_store_bytes
  • pulling/pushing headers with helper functions
  • Operates after SKB creation.

41 of 64

eXpress Data Path (XDP)

  • XDP is the earliest possible programmable hook in the Linux networking stack
  • Runs before SKB allocation inside the driver’s RX path
  • Allows ultra-low-latency packet processing at line rate
  • Based on eBPF programs verified and JIT-compiled
  • Used for: DDoS mitigation, load balancing, firewalls, telemetry

42 of 64

xdp_md

  • Minimal context compared to SKB-based hooks
  • data and data_end pointers define accessible packet region
  • No SKB helpers — raw packet access only
  • Metadata area available for extensions
  • Return value decides packet fate

struct xdp_md {

__u32 data; --> Packet start pointer

__u32 data_end; --> Packet end pointer

__u32 data_meta; --> Metadata region (optional)

__u32 ingress_ifindex;

__u32 rx_queue_index;

};

43 of 64

XDP Actions

ACTION

Description

XDP_ABORTED

Program error, drop with warning

XDP_DROP

Silently drop packet

XDP_PASS

Continue normal stack processing

XDP_TX

Send packet out of same interface (hairpin)

XDP_REDIRECT

Redirect to another interface/CPU/map

44 of 64

Different modes of XDP

Native Mode:

  • Runs in driver RX path
  • Highest performance
  • Requires driver support

Generic Mode:

  • Runs after SKB creation
  • Works without driver support
  • Slower, mainly for debugging

Offload Mode:

  • The eBPF program is offloaded entirely from the host CPU and executed directly on the SmartNIC hardware itself.
  • This mode offers the maximum possible performance and frees up host CPU cycles, but it is dependent on specialized hardware support.

45 of 64

Networking specific maps

Xskmap: The xskmap is used to redirect raw XDP network frames directly to user space via AF_XDP sockets (XSKs), completely bypassing the standard Linux networking stack. This allows for extremely high-speed packet processing in user-space applications. User space creates an AF_XDP socket and inserts its file descriptor into the map using an integer key (typically the queue ID). An XDP eBPF program then uses the bpf_redirect_map() helper to forward incoming packets to the corresponding socket specified by the key.

Cpumap: The cpumap is used to implement software-based Receive Side Scaling (RSS) or to offload packet processing from one CPU to another. This enables efficient distribution of network traffic across multiple CPU cores, particularly useful on hardware that lacks native RSS support. An XDP program running on the initial receiving CPU redirects the packet (using bpf_redirect_map()) to an entry in the cpumap. Each entry in the map is associated with a dedicated kernel thread running on a different, "remote" CPU. This remote CPU handles the rest of the processing, including allocating the necessary SKB (socket buffer) and feeding it into the normal network stack. A second XDP program can even be attached to the remote CPU entry for further processing.

46 of 64

AF_XDP

  • AF_XDP exposes a user-space friendly, extremely fast, zero-copy path from NIC → user program using a shared memory region called UMEM divided into equal frames.
  • This particular socket is bound to a network device queue id.
  • Four lock-free ring buffers (two associated with UMEM: FILL & COMPLETION, and two associated with each socket: RX & TX) pass frame indices and lengths between kernel and user.
  • The user mmap()s the UMEM and the ring control structures, enqueues frame indices into the FILL ring so the kernel can DMA incoming packets into those frames, consumes completed RX descriptors from the RX ring, and returns used frames either to the COMPLETION ring (for TX) or back to the FILL ring for RX reuse.

47 of 64

AF_XDP (Contd)

UMEM is a contiguous user-space memory region that you register with the kernel for AF_XDP. The UMEM is:

  • divided into fixed-size frames (frame size chosen by the application; e.g. 2K, 4K, 2048, 4096 — must meet driver alignment requirements),
  • mmap()'d by the user process, so the application has direct access to the packet buffers, and
  • registered with the kernel via the setsockopt XDP_UMEM_REG syscall
  • one can allocate the UMEM with malloc, posix_memalign, or preferably with mmap using hugepages for better TLB behavior. The memory must remain resident and mapped while in use.

UMEM:

  • UMEM lives in user-space address space but the kernel uses the physical pages for DMA as directed by page pinning/registration during XDP_UMEM_REG.
  • The UMEM consists of N equal frames — each frame holds one chunk of data; frames are referred to by their offset (address) in the UMEM (not absolute pointers in rings).
  • There can be multiple sockets (XSKs), possibly across multiple interfaces, sharing one UMEM if XDP_SHARED_UMEM is used; there is exactly one FILL and one COMPLETION ring per UMEM (shared among sockets), while each socket has its own RX and TX rings. That implies a single process should coordinate access to the UMEM’s FILL/COMPLETION rings.

48 of 64

AF_XDP (Contd)

49 of 64

UMEM

A frame (UMEM slot) cycles through ownership states. Typical lifecycle for receiving a packet:

  • User-space owns frame: user puts frame index into FILL ring (produces it) and advances the producer pointer.
  • Kernel consumes FILL: kernel dequeues from FILL and associates the frame with a NIC RX descriptor; when hardware DMA writes a packet into the frame, kernel will enqueue a descriptor onto the RX ring.
  • User consumes RX: user dequeues frame offset (and length) from RX, processes packet data in UMEM, then either:
  • recycle it for RX: place the frame back on FILL ring so kernel may reuse it for subsequent incoming packets; or
  • transmit it (forward): place it on the TX ring (or copy it into another UMEM frame) — if you send it, kernel will later place its frame address on COMPLETION when transmit completes.
  • Kernel transmits / completes TX: after the NIC transmits, the kernel places the frame index on the COMPLETION ring to return ownership to user-space.
  • User reclaims completion: user reads COMPLETION and can either reuse the frame for TX again or place it on FILL for receiving.

Thus ownership transfers are always mediated by these rings.

Key invariants / rules:

  • Never put the same frame simultaneously on both FILL and TX rings. If you do, the NIC may DMA into a buffer you’re trying to send — data corruption.
  • Never reuse a frame until it is returned to you (via RX consumption or COMPLETION).
  • UMEM frames are not valid for read/write until the owner indicates it (via ring semantics).

50 of 64

Typical AF_XDP (userspace)

  • Allocate UMEM memory (mmap / hugepages).
  • Register UMEM with the kernel (XDP_UMEM_REG).
  • Create AF_XDP socket(s) and set ring sizes via setsockopt.
  • Bind socket to network interface + queue id (bind() with sockaddr_xdp).
  • mmap() the rings control area and UMEM memory.
  • Populate FILL ring with an initial batch of frame offsets (so kernel can receive).
  • Poll (epoll/poll/select) on socket FD or use busy-poll to be notified of RX/TX activity.
  • In the loop: reclaim COMPLETION, replenish FILL, consume RX, produce TX as needed, and submit rings to kernel.

51 of 64

TC-BPF vs XDP

XDP:

  • Earlier
  • Faster
  • No SKB overhead
  • Limited context
  • Ideal for drops, forwarding, filtering

TC:

  • Rich context via SKB
  • Slower
  • Full set of metadata
  • Best for shaping & QoS

Feature

TC-BPF

XDP

Operates on

SKB

Raw frame (XDP buffer)

Runs

Later

Earliest possible

Can reshape traffic

Yes

No

Can modify SKB metadata

Yes

No SKB at all

Speed

Slower than XDP

Fastest

Use cases

shaping, classification

filtering, early drop, AF_XDP redirect, L{2-4} load balancing

52 of 64

Deep Packet Inspection

  • DPI is a data processing technique that goes beyond basic header analysis of network packets. It examines the actual content (payload) of data packets as they cross a checkpoint
  • Layer 3 and Beyond: Unlike traditional packet filtering, which typically only looks at the network and transport layers (IP addresses, ports), DPI operates up to the Application Layer (Layer 7 of the OSI model) to identify the specific application, service, or content being transmitted
  • It provides a highly granular view of network traffic, allowing systems to identify specific applications like YouTube streaming versus standard web browsing, or even identify a specific file transfer.

53 of 64

54 of 64

Bpftool - Lifeline in debugging eBPF

  • bpftool prog show — list programs
  • bpftool prog dump jited — view JIT-compiled assembly
  • bpftool prog tracelog — view verifier logs
  • bpftool map dump — inspect map contents
  • Allows live debugging of running eBPF programs

55 of 64

pL4S - Low Latency Low Loss Scalable Throughput

56 of 64

IPv6 Extension Header

  • Use TC-BPF to read the extension headers not currently supported in the kernel.
  • Use eBPF maps to persist information as necessary.
  • Use TC-BPF to insert/update extension headers not currently supported in the kernel.

57 of 64

Traffic Shaping

  • Token Bucket Policer: One can implement a rate limiting policy using TC-BPF ingress hook.
  • You can drop the packet as needed based on the counters being tracked.
  • The same concept can be applied to any other requirement, such as, Quality of Service (QoS) based on certain parameters.

# create clsact qdisc

tc qdisc add dev eth0 clsact

# attach BPF to ingress (direct-action)

tc filter add dev eth0 ingress bpf da obj tb_policer.o sec classifier

58 of 64

Traffic Shaping

  • For egress shaping, you can use TC-BPF, (cls_bpf) to mark or update the `skb` info.
  • For real scheduling you want a qdisc to own the queues. Use BPF to set skb->priority or mark and let HTB or any other QDisc of your choice to schedule.
  • This marking/update of the `skb` will then translate to actual queuing and shaping by the actual QDisc implementation.
  • Certain times, you need to mark based on some niche or business specific logic and this is very helpful in those scenarios.

59 of 64

eBPF Program Performance

  • XDP programs must be extremely fast due to per-packet execution
  • Key bottlenecks: map lookups, helper calls, tail calls
  • Per-CPU maps = fastest
  • Hash maps have locking overhead (unless per-CPU)
  • Avoid unneeded parsing, loops, and branches
  • Inline packet parsing logic carefully

60 of 64

Verifier Performance

  • Verifier ensures safety but also impacts performance
  • Excessively complex CFG increases verification time
  • Tail calls help modularize programs to reduce verifier state explosion
  • Avoid deep call chains or complex pointer arithmetic
  • Loops must be bounded; verifier must prove termination

61 of 64

Interpreting eBPF JIT Output

if (cursor + sizeof(struct ethhdr) > data_end) return XDP_ABORTED;

eth = cursor;

if (cursor + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end)

return XDP_DROP;

10: mov rax,[r1]

14: mov rcx,[r1+8]

18: add rax,0xe

1c: cmp rax,rcx

1f: jae abort

25: mov rax,[r1]

29: add rax,0x22

2d: cmp rax,rcx

30: jae drop

cursor += sizeof(struct ethhdr) + sizeof(struct iphdr);

if (cursor > data_end)

return XDP_DROP;

10: mov rax,[r1]

14: mov rcx,[r1+8]

18: add rax,0x22

1c: cmp rax,rcx

1f: jae drop

62 of 64

Interpreting eBPF JIT Output

if (cursor + sizeof(struct ethhdr) > data_end) return XDP_ABORTED;

eth = cursor;

if (cursor + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end)

return XDP_DROP;

10: mov rax,[r1]

14: mov rcx,[r1+8]

18: add rax,0xe

1c: cmp rax,rcx

1f: jae abort

25: mov rax,[r1]

29: add rax,0x22

2d: cmp rax,rcx

30: jae drop

cursor += sizeof(struct ethhdr) + sizeof(struct iphdr);

if (cursor > data_end)

return XDP_DROP;

10: mov rax,[r1]

14: mov rcx,[r1+8]

18: add rax,0x22

1c: cmp rax,rcx

1f: jae drop

63 of 64

eBPF Limitations

Guaranteed Termination: Programs must be proven to terminate. Unbounded loops or infinite recursion are not allowed, though bounded loops (where the maximum iteration count can be determined statically) are now supported.

Resource Limits: Programs are subject to size and complexity limits. The current instruction count limit per execution path is approximately one million instructions (as of 2025), which the verifier uses to ensure performance and safety.

Memory Safety: Programs cannot access arbitrary kernel or user memory. Memory accesses must be within valid bounds and are validated using helper functions like bpf_probe_read_user or by accessing data within the program's defined context.

Restricted Function Calls: eBPF programs can only call a predefined set of stable, version-agnostic kernel "helper" functions. They cannot call arbitrary kernel functions or use the full kernel API, which aids compatibility across different kernel versions.

Privilege Requirements: Loading eBPF programs typically requires root privileges or the CAP_BPF Linux capability. Unprivileged eBPF is available but comes with stricter limitations on functionality and access.

Complexity of Development: Writing safe and efficient eBPF code requires a deep understanding of kernel internals, the eBPF virtual machine, and the nuances of the verifier.

Limited Stack Space: The maximum stack space for an eBPF program is limited to 512 bytes. This necessitates using eBPF maps to store larger data structures or pass data between different programs or to user space, adding complexity.

Kernel Version Dependency: eBPF features and verifier behavior can change between Linux kernel versions, leading to potential compatibility issues in mixed environments.

Debugging Difficulties: Debugging eBPF programs can be challenging because they run inside the kernel with limited access to traditional debugging tools used for user-space applications.

Time-of-Check to Time-of-Use (TOCTOU) Issues: Due to the lack of concurrency primitives (like reliable locks that can be acquired from eBPF) and concurrent execution, programs may encounter race conditions when reading kernel data.

64 of 64

Thank you!

Questions?��Let’s discuss!