eBPF lets you run small, verified programs inside the Linux kernel without writing a kernel module or rebooting. The kernel checks every program with a verifier before loading it, so a buggy program is rejected instead of crashing the machine. That makes eBPF the foundation of modern observability tools (bpftrace, BCC), networking (Cilium, XDP load balancers) and runtime security (Falco, Tetragon).

In this tutorial you will check that your Ubuntu 24.04 server supports eBPF, use ready-made BCC tools and bpftrace one-liners to observe the system, inspect loaded programs with bpftool, and write and attach your own XDP program that drops ICMP packets.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS with the stock Ubuntu kernel (6.8 or later), for example a CubePath VPS.
  • A non-root user with sudo privileges. Loading eBPF programs requires root (or the CAP_BPF and CAP_PERFMON capabilities).
  • A second machine you can ping the server from, for the XDP step.

How eBPF works

Before running commands, it helps to know the moving parts:

ComponentRole
ProgramBytecode attached to a hook: a kernel function (kprobe), a tracepoint, a network interface (XDP, tc), a cgroup or an LSM hook.
VerifierChecks every program at load time: bounded loops, valid memory access, no uninitialized reads. Programs that fail are rejected.
JIT compilerTranslates verified bytecode into native machine code, so programs run at near-native speed.
MapsKey/value structures shared between the kernel program and user space (hash, array, ring buffer, per-CPU variants). Used for counters, histograms and configuration.
BTFType information about the running kernel. It lets programs be compiled once and run on different kernel versions (CO-RE).

The typical use cases map to hook types:

  • Observability: kprobes and tracepoints to measure latency, count syscalls, trace process execution.
  • Networking: XDP and tc programs to filter, redirect or load balance packets before they reach the normal network stack.
  • Security: LSM and tracepoint programs to audit or block file access and process execution.

Step 1 - Checking kernel support

Ubuntu 24.04 ships a kernel with eBPF, the JIT and BTF enabled. Confirm the kernel version and that BTF information is present:

uname -r
ls -l /sys/kernel/btf/vmlinux
6.8.0-45-generic
-r--r--r-- 1 root root 5874412 Sep 25 10:00 /sys/kernel/btf/vmlinux

Check that the JIT compiler is enabled (1 means on):

cat /proc/sys/net/core/bpf_jit_enable
1

Unprivileged users are not allowed to load eBPF programs by default. Leave it like that; a value of 2 is the secure default:

cat /proc/sys/kernel/unprivileged_bpf_disabled
2

Step 2 - Installing the eBPF toolchain

Install the three tool sets used in this tutorial:

  • bpfcc-tools: the BCC collection of ready-to-use tracing tools.
  • bpftrace: a high-level tracing language for one-liners and short scripts.
  • linux-tools-$(uname -r): provides bpftool for inspecting loaded programs and maps.

Also install clang, llvm and libbpf-dev, which you will need to compile your own program in Step 5:

sudo apt update
sudo apt install bpfcc-tools bpftrace linux-tools-common linux-tools-$(uname -r) clang llvm libbpf-dev

Verify the installation:

bpftrace --version
sudo bpftool version
bpftrace v0.20.2
bpftool v7.4.0
using libbpf v1.4

On Ubuntu the BCC tools carry a -bpfcc suffix, for example execsnoop-bpfcc. List them with ls /usr/sbin/*-bpfcc.

Step 3 - Observing the system with BCC tools

BCC tools answer common questions without writing any code. Each one loads an eBPF program, prints events until you press Ctrl+C, then unloads it.

Trace every new process started on the server. This is useful to catch short-lived commands spawned by cron jobs or scripts that ps never sees:

sudo execsnoop-bpfcc

In another terminal, run any command such as ls /tmp. The first terminal shows it:

PCOMM            PID     PPID    RET ARGS
ls               41822   41790     0 /usr/bin/ls --color=auto /tmp

Trace which files are opened, filtered to one process name:

sudo opensnoop-bpfcc -n nginx

Trace outgoing TCP connections, including the destination and port. This quickly reveals what an application talks to:

sudo tcpconnect-bpfcc
PID     COMM         IP SADDR            DADDR            DPORT
41901   curl         4  203.0.113.10     93.184.215.14    443

Measure block device I/O latency as a histogram, printed every 5 seconds, twice:

sudo biolatency-bpfcc 5 2
     usecs               : count     distribution
        64 -> 127        : 112      |****************                        |
       128 -> 255        : 276      |****************************************|
       256 -> 511        : 58       |********                                |

Step 4 - Writing bpftrace one-liners

bpftrace is ideal when no ready-made tool answers your question. Its syntax is probe /filter/ { action }.

Count system calls per process for 10 seconds:

sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); } interval:s:10 { exit(); }'
@[sshd]: 214
@[systemd-journal]: 388
@[nginx]: 1502

Print every file opened on the system with the process that opened it:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%-16s %s\n", comm, str(args->filename)); }'

Show the distribution of read() sizes returned by the kernel as a power-of-two histogram. Press Ctrl+C to print it:

sudo bpftrace -e 'kretprobe:vfs_read { @bytes = hist(retval); }'

To list the probes you can attach to, search by pattern:

sudo bpftrace -l 'tracepoint:syscalls:sys_enter_open*'

Step 5 - Filtering packets with XDP

XDP (eXpress Data Path) runs an eBPF program on every packet as soon as the network driver receives it, before the kernel allocates any socket buffer. That is why XDP is used for DDoS mitigation and fast load balancers. You will write a program that drops IPv4 ICMP packets and lets everything else through, so your SSH session is not affected.

Create a working directory and the source file:

mkdir -p ~/xdp-demo && cd ~/xdp-demo
nano xdp_drop_icmp.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

SEC("xdp")
int xdp_drop_icmp(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    if (ip->protocol == IPPROTO_ICMP)
        return XDP_DROP;

    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

Every pointer is checked against data_end before it is read. Without those checks the verifier refuses to load the program, because it cannot prove the access stays inside the packet.

Compile it to eBPF bytecode. The extra include path points clang at the architecture-specific kernel headers:

clang -O2 -g -target bpf -I/usr/include/$(uname -m)-linux-gnu -c xdp_drop_icmp.c -o xdp_drop_icmp.o
file xdp_drop_icmp.o
xdp_drop_icmp.o: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV), with debug_info, not stripped

Find the name of your public interface:

ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0             UP             52:54:00:12:34:56 <BROADCAST,MULTICAST,UP,LOWER_UP>

Before attaching, start a ping to the server from your second machine and leave it running. Then attach the program to the interface (replace eth0 with yours). The xdpgeneric mode works with any driver, including virtio on virtual machines:

sudo ip link set dev eth0 xdpgeneric obj xdp_drop_icmp.o sec xdp

The ping on the other machine stops receiving replies, while your SSH session keeps working. Confirm the program is attached:

ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff
    prog/xdp id 87 name xdp_drop_icmp tag 3b185187f1855c4c jited

Detach it, and the ping replies return:

sudo ip link set dev eth0 xdpgeneric off

Step 6 - Inspecting loaded programs with bpftool

bpftool shows every eBPF program and map currently loaded, including those installed by systemd, container runtimes or security agents. List programs:

sudo bpftool prog show
42: cgroup_device  name sd_devices  tag 3a0ef5414c2f6fca  gpl
	loaded_at 2026-09-25T09:58:12+0000  uid 0
	xlated 504B  jited 309B  memlock 4096B

List maps:

sudo bpftool map show

Show which programs are attached to network interfaces:

sudo bpftool net show

Programs appear here only while attached. When a BCC tool or bpftrace exits, its programs are removed automatically.

Troubleshooting

clang fails with 'asm/types.h' file not found. The architecture include path is missing. Keep the -I/usr/include/$(uname -m)-linux-gnu flag shown in Step 5.

The verifier rejects the program with invalid access to packet. A packet pointer is read without a bounds check against data_end. Add the check before every header access. Run sudo bpftool prog load xdp_drop_icmp.o /sys/fs/bpf/test to see the full verifier log, then remove the pinned file with sudo rm /sys/fs/bpf/test.

ERROR: Could not resolve symbol or missing probes in bpftrace. The function may be inlined or renamed in your kernel. Search for the right name with sudo bpftrace -l 'kprobe:*read*'.

linux-tools-$(uname -r) cannot be found. You are running a custom or non-Ubuntu kernel. Boot the stock Ubuntu kernel, or install the matching tools package from your kernel vendor.

Conclusion

You checked eBPF support on Ubuntu 24.04, used BCC tools and bpftrace to observe processes, files, network connections and disk latency, and wrote an XDP program that filters packets before they reach the network stack. Good next steps are reading the BCC tool list with man execsnoop-bpfcc and its siblings, writing CO-RE programs with libbpf and a user-space loader that reads maps, and exploring projects built on eBPF such as Cilium for Kubernetes networking or Falco for runtime security.