eBPF: Introducción y Casos de Uso - Guía del Filtro de Paquetes Berkeley Extendido
Introducción
eBPF (Filtro de Paquetes Berkeley Extendido) representa una tecnología revolucionaria que transforma fundamentalmente cómo interactuamos con el kernel de Linux. Originalmente diseñado para filtrado de paquetes, eBPF ha evolucionado en un marco completo que permite a los desarrolladores ejecutar programas en sandbox dentro del kernel sin modificar el código fuente del kernel o cargar módulos del kernel. Esta capacidad desbloquea posibilidades de observabilidad, seguridad y redes sin precedentes que anteriormente eran imposibles o requerían modificaciones arriesgadas del kernel.
Grandes empresas tecnológicas incluyendo Facebook, Netflix, Cloudflare y Google aprovechan eBPF extensivamente para monitoreo de rendimiento, aplicación de seguridad, balanceo de carga y optimización de red. Cloudflare procesa millones de paquetes por segundo usando XDP (eXpress Data Path) impulsado por eBPF para mitigación de DDoS. Netflix monitorea el rendimiento de aplicaciones a través de decenas de miles de servidores usando rastreo basado en eBPF. Cilium, la solución líder de redes y seguridad nativa en la nube, usa eBPF como su tecnología central para redes de Kubernetes y aplicación de políticas de seguridad.
La importancia de eBPF se extiende más allá de mejoras incrementales—cambia fundamentalmente cómo abordamos la programación del kernel, observabilidad y seguridad. El desarrollo tradicional del kernel requería meses de pruebas y validación antes del despliegue en producción. eBPF permite instrumentación dinámica, permitiendo a los ingenieros desplegar soluciones de monitoreo, rastreo y seguridad en minutos sin reinicios, compilación de módulos del kernel o reinicio del sistema.
Esta guía completa explora fundamentos de eBPF, arquitectura, casos de uso prácticos, flujos de trabajo de desarrollo, optimización de rendimiento, implicaciones de seguridad y metodologías de solución de problemas esenciales para aprovechar esta tecnología transformadora en entornos de producción.
Teoría y Conceptos Fundamentales
Arquitectura eBPF
eBPF proporciona una máquina virtual dentro del kernel de Linux:
Máquina Virtual eBPF: Ejecuta bytecode eBPF con compilación JIT (Just-In-Time) a código máquina nativo para rendimiento. Proporciona un entorno de ejecución seguro con:
- Bucles Limitados: Previene bucles infinitos asegurando terminación
- Seguridad de Memoria: El verificador asegura ningún acceso no autorizado a memoria
- Pila Limitada: Pila de 512 bytes previene agotamiento de recursos
- Funciones Auxiliares: Acceso controlado a funciones del kernel vía auxiliares en lista blanca
Verificador eBPF: Analiza programas antes de la ejecución asegurando:
- Todos los caminos de código terminan (sin bucles infinitos)
- Sin acceso no autorizado a memoria
- Corrección en el uso de registros
- Verificación de límites apropiada
- Seguridad de tipos
Mapas eBPF: Estructuras de datos del kernel que permiten:
- Comunicación entre kernel y espacio de usuario
- Compartir datos entre programas eBPF
- Recopilación y agregación de estadísticas
- Almacenamiento de configuración
Tipos de Mapas:
- Mapas Hash: Almacenamiento clave-valor
- Arrays: Almacenamiento indexado de tamaño fijo
- Arrays de Programas: Almacenan otros programas eBPF
- Mapas Por-CPU: Datos específicos de CPU evitando sincronización
- Buffers de Anillo: Streaming eficiente de eventos a espacio de usuario
- Mapas LRU: Desalojo automático de entradas menos recientemente usadas
Tipos de Programas eBPF
Diferentes tipos de programas se adjuntan a varios hooks del kernel:
Filtros de Socket (BPF_PROG_TYPE_SOCKET_FILTER): Filtrado clásico de paquetes adjunto a sockets. Caso de uso original de BPF.
Kprobes/Kretprobes: Instrumentación dinámica de funciones del kernel. Permite rastrear cualquier función del kernel incluyendo valores de entrada y retorno.
Tracepoints: Puntos de instrumentación estática mantenidos a través de versiones del kernel. Más estables que kprobes.
XDP (eXpress Data Path): Procesa paquetes en el punto más temprano después de la recepción del controlador. Permite procesamiento de paquetes de alto rendimiento para mitigación de DDoS, balanceo de carga, filtrado de paquetes.
TC (Control de Tráfico): Se engancha al subsistema TC de Linux para manipulación de paquetes, redirección y QoS.
Programas Cgroup: Se adjuntan a eventos de cgroup para control de recursos, control de acceso y monitoreo.
Hooks LSM (Módulo de Seguridad de Linux): Aplicación de seguridad en hooks LSM. Permite políticas de seguridad personalizadas.
Eventos Perf: Perfila eventos de rendimiento de CPU, eventos de hardware, eventos de software.
eBPF vs Enfoques Tradicionales
Entendiendo las ventajas de eBPF sobre alternativas:
vs Módulos del Kernel:
- eBPF: Seguro, verificado, sin recompilación del kernel, desplegable en caliente
- Módulos del Kernel: Inseguro, puede bloquear el kernel, requiere compilación, necesita reinicio
vs Procesamiento en Espacio de Usuario:
- eBPF: Se ejecuta en el kernel, cero cambios de contexto, alto rendimiento
- Espacio de Usuario: Sobrecarga de cambio de contexto, más lento, visibilidad limitada del kernel
vs SystemTap/DTrace:
- eBPF: Integrado en el kernel de Linux, portable, herramientas estándar
- SystemTap: Requiere debuginfo del kernel, configuración compleja, problemas de estabilidad
Arquitectura XDP
XDP merece atención especial para casos de uso de redes:
Etapas de Procesamiento XDP:
- Modo Controlador: Más rápido, procesa paquetes antes de la asignación de sk_buff
- Modo Genérico: Respaldo, procesa después de la asignación de sk_buff
- Modo Descarga: Descarga el procesamiento al hardware NIC (requiere soporte de hardware específico)
Acciones XDP:
- XDP_DROP: Descartar paquete inmediatamente (mitigación DDoS)
- XDP_PASS: Continuar procesamiento normal del kernel
- XDP_TX: Transmitir paquete de vuelta en la interfaz receptora
- XDP_REDIRECT: Redirigir a diferente interfaz o CPU
- XDP_ABORTED: Condición de error, descartar paquete y rastrear
Requisitos Previos
Requisitos del Kernel
Versión Mínima del Kernel: 4.4+ (5.x+ recomendado para soporte completo de características)
Verificar Soporte eBPF:
# Check kernel version
uname -r
# Verify eBPF support
cat /proc/config.gz | gunzip | grep BPF
# or
grep BPF /boot/config-$(uname -r)
# Required options:
# CONFIG_BPF=y
# CONFIG_BPF_SYSCALL=y
# CONFIG_BPF_JIT=y
# CONFIG_HAVE_EBPF_JIT=y
Habilitar BPF JIT (si no está habilitado):
echo 1 > /proc/sys/net/core/bpf_jit_enable
# Make persistent
echo "net.core.bpf_jit_enable = 1" >> /etc/sysctl.d/99-bpf.conf
Requisitos de Software
Herramientas de Desarrollo:
RHEL/Rocky/CentOS:
# Install LLVM/Clang for BPF compilation
dnf install -y clang llvm
# Install development headers
dnf install -y kernel-devel kernel-headers
# Install BPF compiler collection (BCC)
dnf install -y bcc-tools python3-bcc
# Install bpftool
dnf install -y bpftool
# Install libbpf development
dnf install -y libbpf-devel
Ubuntu/Debian:
# Install LLVM/Clang
apt update
apt install -y clang llvm libelf-dev
# Install kernel headers
apt install -y linux-headers-$(uname -r)
# Install BCC
apt install -y bpfcc-tools python3-bpfcc
# Install bpftool
apt install -y linux-tools-common linux-tools-$(uname -r)
# Install libbpf
apt install -y libbpf-dev
Verificar Instalación:
# Check clang version (10+ recommended)
clang --version
# Verify bpftool
bpftool version
# Test BCC installation
python3 -c "import bcc; print('BCC installed successfully')"
Configuración Avanzada
Programa eBPF Hello World (BCC)
Ejemplo simple de rastreo:
#!/usr/bin/env python3
# hello_world.py - Trace execve syscall
from bcc import BPF
# BPF program
program = """
#include <uapi/linux/ptrace.h>
int trace_execve(struct pt_regs *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("Process %s called execve\\n", comm);
return 0;
}
"""
# Load BPF program
b = BPF(text=program)
# Attach to execve syscall
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="trace_execve")
print("Tracing execve syscall... Press Ctrl-C to exit")
# Read trace pipe
try:
b.trace_print()
except KeyboardInterrupt:
print("Exiting")
Ejecutar:
chmod +x hello_world.py
sudo ./hello_world.py
Filtrado de Paquetes XDP
Filtro de paquetes de alto rendimiento:
// xdp_filter.c - Drop packets from specific IP
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#define BLOCKED_IP 0x0A000001 // 10.0.0.1 in network byte order
SEC("xdp")
int xdp_filter_prog(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// Parse Ethernet header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
// Check if IPv4
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
// Parse IP header
struct iphdr *ip = (struct iphdr *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
// Drop packets from blocked IP
if (ip->saddr == BLOCKED_IP) {
return XDP_DROP;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Compilar y cargar:
# Compile to BPF bytecode
clang -O2 -target bpf -c xdp_filter.c -o xdp_filter.o
# Load XDP program
sudo ip link set dev eth0 xdp obj xdp_filter.o sec xdp
# Verify loaded
sudo ip link show dev eth0
# Remove XDP program
sudo ip link set dev eth0 xdp off
Rastreo de Conexiones TCP
Monitorear conexiones TCP:
#!/usr/bin/env python3
# tcp_tracker.py - Track TCP connections
from bcc import BPF
from socket import inet_ntop, AF_INET
import struct
# BPF program
program = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>
// Data structure for events
struct conn_event {
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u32 state;
};
BPF_PERF_OUTPUT(events);
int trace_tcp_state_change(struct pt_regs *ctx, struct sock *sk, int oldstate, int newstate) {
if (newstate != TCP_ESTABLISHED && newstate != TCP_CLOSE)
return 0;
struct conn_event event = {};
// Extract connection information
u16 family = sk->__sk_common.skc_family;
if (family != AF_INET)
return 0;
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.state = newstate;
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
"""
# Load BPF program
b = BPF(text=program)
# Attach to TCP state change function
b.attach_kprobe(event="tcp_set_state", fn_name="trace_tcp_state_change")
# Event handler
def print_event(cpu, data, size):
event = b["events"].event(data)
saddr = inet_ntop(AF_INET, struct.pack("I", event.saddr))
daddr = inet_ntop(AF_INET, struct.pack("I", event.daddr))
sport = event.sport
dport = event.dport
state = "ESTABLISHED" if event.state == 1 else "CLOSE"
print(f"{saddr}:{sport} -> {daddr}:{dport} [{state}]")
# Open perf buffer
b["events"].open_perf_buffer(print_event)
print("Tracking TCP connections... Press Ctrl-C to exit")
try:
while True:
b.perf_buffer_poll()
except KeyboardInterrupt:
print("Exiting")
Monitoreo de Acceso a Archivos
Ejemplo de monitoreo de seguridad:
#!/usr/bin/env python3
# file_monitor.py - Monitor file access
from bcc import BPF
program = """
#include <uapi/linux/ptrace.h>
#include <linux/fs.h>
struct file_info {
u32 pid;
char comm[16];
char filename[256];
};
BPF_PERF_OUTPUT(events);
int trace_open(struct pt_regs *ctx, struct file *file) {
struct file_info info = {};
info.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&info.comm, sizeof(info.comm));
// Get filename
struct dentry *dentry = file->f_path.dentry;
bpf_probe_read_kernel_str(&info.filename, sizeof(info.filename),
(void *)dentry->d_name.name);
// Filter: only monitor /etc/ directory
if (info.filename[0] == 'e' &&
info.filename[1] == 't' &&
info.filename[2] == 'c' &&
info.filename[3] == '/') {
events.perf_submit(ctx, &info, sizeof(info));
}
return 0;
}
"""
b = BPF(text=program)
b.attach_kprobe(event="security_file_open", fn_name="trace_open")
def print_event(cpu, data, size):
event = b["events"].event(data)
print(f"PID {event.pid} ({event.comm.decode()}) opened: {event.filename.decode()}")
b["events"].open_perf_buffer(print_event)
print("Monitoring /etc/ directory access... Press Ctrl-C to exit")
try:
while True:
b.perf_buffer_poll()
except KeyboardInterrupt:
print("Exiting")
Monitoreo de Rendimiento
Perfilado de CPU con eBPF:
#!/usr/bin/env python3
# cpu_profile.py - CPU profiler using eBPF
from bcc import BPF
from time import sleep
program = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct key_t {
char name[16];
int stack_id;
};
BPF_HASH(counts, struct key_t);
BPF_STACK_TRACE(stack_traces, 10000);
int on_cpu_sample(struct bpf_perf_event_data *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct key_t key = {};
bpf_get_current_comm(&key.name, sizeof(key.name));
key.stack_id = stack_traces.get_stackid(&ctx->regs, BPF_F_USER_STACK);
counts.increment(key);
return 0;
}
"""
b = BPF(text=program)
b.attach_perf_event(ev_type=PerfType.SOFTWARE,
ev_config=PerfSWConfig.CPU_CLOCK,
fn_name="on_cpu_sample",
sample_period=0,
sample_freq=49)
print("Profiling CPU usage for 10 seconds...")
sleep(10)
# Print results
counts = b.get_table("counts")
stack_traces = b.get_table("stack_traces")
print("\nTop CPU consumers:")
for k, v in sorted(counts.items(), key=lambda kv: kv[1].value, reverse=True)[:20]:
print(f"{k.name.decode():16s} {v.value:10d}")
if k.stack_id >= 0:
stack = list(stack_traces.walk(k.stack_id))
for addr in stack:
print(f" {b.sym(addr, -1).decode()}")
Optimización de Rendimiento
Optimización de Mapas
Elige tipos de mapas apropiados:
// Per-CPU map for high-frequency updates (avoids locks)
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 10000);
__type(key, u32);
__type(value, u64);
} stats SEC(".maps");
// LRU map for automatic eviction
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 10000);
__type(key, u32);
__type(value, struct data);
} lru_cache SEC(".maps");
// Array for fixed-size indexed data
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 256);
__type(key, u32);
__type(value, u64);
} counters SEC(".maps");
Compilación JIT
Asegúrate de que JIT esté habilitado:
# Enable BPF JIT
echo 1 > /proc/sys/net/core/bpf_jit_enable
# Enable BPF JIT hardening (slight performance cost)
echo 2 > /proc/sys/net/core/bpf_jit_harden
# View JIT statistics
cat /proc/sys/net/core/bpf_jit_enable
Selección de Funciones Auxiliares
Usa funciones auxiliares eficientes:
// Fast: use bpf_ktime_get_ns() for timestamps
u64 timestamp = bpf_ktime_get_ns();
// Slow: avoid string operations in hot paths
// bpf_probe_read_str() can be expensive
// Fast: use per-CPU maps for statistics
struct data *d = bpf_map_lookup_elem(&percpu_map, &key);
if (d) {
d->counter++;
}
Operaciones por Lotes
Actualiza mapas eficientemente:
# Inefficient: update one by one
for i in range(1000):
b["map"][ct.c_uint(i)] = ct.c_uint(i * 2)
# Efficient: batch update
keys = (ct.c_uint * 1000)(*range(1000))
values = (ct.c_uint * 1000)(*[i * 2 for i in range(1000)])
b["map"].items_update_batch(keys, values)
Monitoreo y Observabilidad
Inspección de Programas BPF
# List loaded BPF programs
bpftool prog show
# Show program details
bpftool prog show id 42
# Dump program bytecode
bpftool prog dump xlated id 42
# Dump program JIT code
bpftool prog dump jited id 42
# Pin program to filesystem
bpftool prog pin id 42 /sys/fs/bpf/my_prog
# Load pinned program
bpftool prog load pinned /sys/fs/bpf/my_prog
Inspección de Mapas
# List BPF maps
bpftool map show
# Show map contents
bpftool map dump id 10
# Update map entry
bpftool map update id 10 key 0x0a 0x00 0x00 0x01 value 0x01 0x00 0x00 0x00
# Delete map entry
bpftool map delete id 10 key 0x0a 0x00 0x00 0x01
# Pin map to filesystem
bpftool map pin id 10 /sys/fs/bpf/my_map
Monitoreo de Rendimiento
# Monitor BPF program run time
bpftool prog profile id 42 duration 30
# Show BPF statistics
cat /proc/sys/kernel/bpf_stats_enabled
echo 1 > /proc/sys/kernel/bpf_stats_enabled
bpftool prog show
Solución de Problemas
Rechazo del Verificador
Síntoma: Programa BPF rechazado por el verificador.
Diagnóstico:
# View verifier log
dmesg | tail -100
# Load with increased log level
bpftool prog load program.o /sys/fs/bpf/prog log_level 2
# Common issues:
# - Unbounded loops
# - Invalid memory access
# - Stack overflow
# - Uninitialized variables
Resolución:
// Bad: unbounded loop
for (int i = 0; i < n; i++) { } // Verifier rejects
// Good: bounded loop
#define MAX_ITER 100
for (int i = 0; i < MAX_ITER && i < n; i++) { }
// Bad: potential NULL dereference
struct data *d = bpf_map_lookup_elem(&map, &key);
d->field = value; // Verifier rejects
// Good: NULL check
struct data *d = bpf_map_lookup_elem(&map, &key);
if (!d)
return 0;
d->field = value;
Problemas de Rendimiento
Síntoma: Alto uso de CPU o latencia de programas BPF.
Diagnóstico:
# Profile BPF program
bpftool prog profile id 42 duration 30
# Check program complexity
bpftool prog show id 42
# Monitor map operations
bpftool map show id 10
Resolución:
# Optimize map type (use per-CPU maps)
# Reduce program complexity
# Use tail calls for complex logic
# Enable JIT compilation
echo 1 > /proc/sys/net/core/bpf_jit_enable
XDP No Funciona
Síntoma: Programa XDP no procesa paquetes.
Diagnóstico:
# Check XDP attachment
ip link show dev eth0
# Verify driver support (native mode)
ethtool -i eth0
# Check for errors
dmesg | grep -i xdp
# Test in generic mode
ip link set dev eth0 xdpgeneric obj program.o sec xdp
Resolución:
# Use supported driver or generic mode
# Check for conflicting XDP programs
ip link set dev eth0 xdp off
# Reload program
ip link set dev eth0 xdp obj program.o sec xdp verbose
Conclusión
eBPF representa un cambio de paradigma en la programación del kernel de Linux, observabilidad y seguridad. Al permitir que programas seguros y verificados se ejecuten dentro del kernel, eBPF desbloquea capacidades previamente imposibles sin modificaciones arriesgadas del kernel—rastreo dinámico, procesamiento de paquetes de alto rendimiento, aplicación de políticas de seguridad y observabilidad completa del sistema.
Las organizaciones que aprovechan eBPF obtienen visibilidad sin precedentes del comportamiento del sistema, rendimiento de red y eventos de seguridad mientras mantienen la estabilidad y seguridad del kernel. El verificador asegura la seguridad del programa, previniendo bloqueos y vulnerabilidades de seguridad que plagan los enfoques tradicionales de módulos del kernel.
La adopción exitosa de eBPF requiere comprender la arquitectura del kernel, fundamentos de redes y técnicas de optimización de rendimiento. Aunque herramientas como BCC simplifican el desarrollo de eBPF, casos de uso avanzados demandan conocimiento más profundo de los internos del kernel, optimización de mapas y selección de funciones auxiliares.
A medida que la computación nativa en la nube, Kubernetes y las arquitecturas de service mesh se vuelven ubicuas, la importancia de eBPF continúa creciendo. Proyectos como Cilium, Falco y Pixie demuestran el potencial transformador de eBPF para redes de contenedores, seguridad y observabilidad. Los ingenieros que dominan eBPF se posicionan a la vanguardia de la innovación de Linux, construyendo plataformas de infraestructura de próxima generación aprovechando esta tecnología revolucionaria.


