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
sudoprivileges. Loading eBPF programs requires root (or theCAP_BPFandCAP_PERFMONcapabilities). - A second machine you can
pingthe server from, for the XDP step.
How eBPF works
Before running commands, it helps to know the moving parts:
| Component | Role |
|---|---|
| Program | Bytecode attached to a hook: a kernel function (kprobe), a tracepoint, a network interface (XDP, tc), a cgroup or an LSM hook. |
| Verifier | Checks every program at load time: bounded loops, valid memory access, no uninitialized reads. Programs that fail are rejected. |
| JIT compiler | Translates verified bytecode into native machine code, so programs run at near-native speed. |
| Maps | Key/value structures shared between the kernel program and user space (hash, array, ring buffer, per-CPU variants). Used for counters, histograms and configuration. |
| BTF | Type 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): providesbpftoolfor 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
NoteOn physical NICs whose driver supports native XDP (for example on a dedicated server with Intel or Mellanox cards), use
xdpdrvinstead ofxdpgenericfor much higher performance. Generic mode is meant for testing and compatibility.
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.
