Tutorial: mitigar ataques DDoS en VPS con eBPF/XDP en Ubuntu 22.04 — paso a paso práctico
Redes & Segurança · Por Kevin Marcondes · Publicado el · 9 min de lectura
Tutorial: mitigar ataques DDoS en VPS con eBPF/XDP en Ubuntu 22.04 — paso a paso. Vas a compilar un programa XDP sencillo, adjuntarlo a la interfaz de red, validar contadores y bloquear IPs con bpftool. Este enfoque de baja latencia filtra tráfico malicioso antes de la pila. Prueba con cuidado y mantén acceso de recuperación.
Requisitos previos, riesgos y objetivo
Vamos a filtrar paquetes en el driver con XDP (Express Data Path) usando eBPF. Requiere acceso root y puede interrumpir la conectividad si se aplica incorrectamente. Prueba en un entorno aislado o ten consola fuera de banda.
El objetivo es bloquear fuentes obvias (IPs spoofeadas y reservadas), descartar fragmentos IP y, opcionalmente, UDP hacia puertos HTTP cuando haya un ataque. Integra esto con prácticas de hardening.
Antes de empezar, asegúrate de lo básico e instala las herramientas.
- Ubuntu Server 22.04 con el kernel por defecto.
- Acceso root o sudo con privilegios.
- Clang/LLVM, headers del kernel, bpftool, iproute2 y xdp-tools.
- Ventana de mantenimiento y acceso alternativo (ej.: consola del proveedor).
- Checklist complementario: consulta el checklist de seguridad para VPS.
sudo apt update
sudo apt install -y clang llvm libbpf-dev libelf-dev build-essential \
linux-headers-$(uname -r) bpftool iproute2 git make pkg-config xdp-tools
Escribe un programa XDP básico de mitigación (C)
El programa siguiente realiza un parseo mínimo de Ethernet e IPv4. Descarta paquetes con IP de origen inválida (RFC1918, loopback, multicast y reservadas) y fragmentos IP. También ofrece la opción de dropear UDP hacia 80/443 durante ataques. Mantiene contadores de drops por CPU.
Ajusta las macros en la parte superior según tu escenario. Si usas QUIC (UDP/443), desactiva el drop de UDP.
// xdp_ddos_kern.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>
// Ajustes rápidos
#define DROP_UDP_TO_HTTP 1 // 1 = drop UDP para portas 80/443; 0 = permitir
#define DROP_FRAGMENTS 1 // 1 = drop de fragmentos IP; 0 = permitir
struct counters { __u64 packets; __u64 bytes; };
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct counters);
} drop_stats SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 16384);
__type(key, __u32); // IPv4 (host-order)
__type(value, __u8); // 1 = bloquear
} blocked_ips SEC(".maps");
static __always_inline int count_drop(__u32 len) {
__u32 key = 0;
struct counters *c = bpf_map_lookup_elem(&drop_stats, &key);
if (c) {
__sync_fetch_and_add(&c->packets, 1);
__sync_fetch_and_add(&c->bytes, len);
}
return XDP_DROP;
}
static __always_inline bool is_bad_src_v4(__u32 s) {
// s em host-order
// 10/8
if ((s & 0xFF000000) == 0x0A000000) return true;
// 172.16/12
if ((s & 0xFFF00000) == 0xAC100000) return true;
// 192.168/16
if ((s & 0xFFFF0000) == 0xC0A80000) return true;
// 127/8 (loopback)
if ((s & 0xFF000000) == 0x7F000000) return true;
// 0/8
if ((s & 0xFF000000) == 0x00000000) return true;
// 169.254/16 (link-local)
if ((s & 0xFFFF0000) == 0xA9FE0000) return true;
// 224/4 (multicast) e 240/4 (reservado)
if ((s & 0xF0000000) == 0xE0000000) return true;
if ((s & 0xF0000000) == 0xF0000000) return true;
return false;
}
SEC("xdp")
int xdp_ddos(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return XDP_PASS; // só IPv4 aqui
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
// Drop por fragmentação
if (DROP_FRAGMENTS) {
if (ip->frag_off & bpf_htons(IP_MF | IP_OFFSET))
return count_drop((__u32)(data_end - data));
}
__u32 saddr = bpf_ntohl(ip->saddr);
if (is_bad_src_v4(saddr))
return count_drop((__u32)(data_end - data));
// Blocklist dinâmica (hash)
__u8 *blocked = bpf_map_lookup_elem(&blocked_ips, &saddr);
if (blocked && *blocked == 1)
return count_drop((__u32)(data_end - data));
// Regras UDP opcionais
if (DROP_UDP_TO_HTTP && ip->protocol == IPPROTO_UDP) {
struct udphdr *udp = (void *)(ip + 1);
if ((void *)(udp + 1) > data_end)
return XDP_PASS;
__u16 dport = bpf_ntohs(udp->dest);
if (dport == 80 || dport == 443) {
return count_drop((__u32)(data_end - data));
}
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Compila el programa para eBPF
Compila el objeto eBPF con clang. El resultado xdp_ddos_kern.o se cargará en la interfaz de red. Ejecuta como root o con sudo. Si cambias macros, recompila antes de recargar.
cat > xdp_ddos_kern.c <<'EOF'
[cole aqui o código C da seção anterior]
EOF
clang -O2 -g -target bpf -c xdp_ddos_kern.c -o xdp_ddos_kern.o
ls -lh xdp_ddos_kern.o
Adjunta el XDP a la interfaz (nativo o genérico)
Identifica la interfaz WAN de la VPS. Intenta el modo nativo (driver) primero. Si el driver no soporta, usa el modo genérico (skb). Este funciona en la mayoría de casos, con mayor overhead.
Atención: adjuntar XDP puede interrumpir conexiones activas. Ten SSH alternativo o consola lista. Para acceso seguro, considera un bastion SSH con 2FA. Si prefieres, el paquete xdp-tools incluye xdp-loader.
IFACE=eth0 # ajuste se necessário
ip -brief link
# Modo nativo (driver). Se falhar, tente o genérico abaixo.
sudo ip link set dev "$IFACE" xdp obj xdp_ddos_kern.o sec xdp
# Alternativa: modo genérico (fallback)
# sudo ip link set dev "$IFACE" xdpgeneric obj xdp_ddos_kern.o sec xdp
Cómo confirmar que funcionó (estado y contadores)
Verifica si el programa XDP está adjuntado y recopila contadores de drops. Genera tráfico de prueba. En producción, monitorea los números y ajusta reglas. Para gráficos, consulta Prometheus y Grafana en VPS.
# Verificar XDP na interface
ih=$(ip -details link show dev "$IFACE" | sed -n '/prog/xdp/,$p')
echo "$ih" | sed -n '1,15p'
# Listar programas e mapas no netns
yes | sudo bpftool net -a
# Achar ID do mapa de contadores e exibir
STATS_ID=$(sudo bpftool map show | awk '/drop_stats/ {print $1}' | sed 's/id //')
echo "Mapa drop_stats id=$STATS_ID"
sudo bpftool map dump id "$STATS_ID"
Bloquea IPs dinámicamente con bpftool (durante el ataque)
Una blocklist funciona en tiempo real. Para añadir una IP de origen sospechosa, actualiza el mapa hash. Usa el ID del mapa blocked_ips. Elimina la entrada cuando termine el ataque. Evita bloqueos amplios que afecten clientes legítimos.
# Descobrir o ID do mapa blocked_ips
MAP_ID=$(sudo bpftool map show | awk '/blocked_ips/ {print $1}' | sed 's/id //')
echo "blocked_ips id=$MAP_ID"
# Exemplo: bloquear 203.0.113.45 (hex: CB 00 71 2D)
sudo bpftool map update id "$MAP_ID" key hex CB 00 71 2D value hex 01
# Conferir
sudo bpftool map lookup id "$MAP_ID" key hex CB 00 71 2D
# Remover da blocklist
# sudo bpftool map delete id "$MAP_ID" key hex CB 00 71 2D
Hazlo persistente con systemd (cargar en el arranque)
Crea un directorio para el objeto y un servicio systemd que adjunte el XDP después de que la red esté online. Ajusta IFACE en el unit file. Para actualizar reglas, recompila y reinicia el servicio.
Cuidado: cambios incorrectos pueden bloquear el servidor en el arranque. Ten acceso de recuperación del proveedor.
sudo mkdir -p /opt/xdp
sudo cp xdp_ddos_kern.o /opt/xdp/
sudo tee /etc/systemd/system/xdp-ddos.service > /dev/null <<'UNIT'
[Unit]
Description=XDP DDoS mitigation (attach program on interface)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/ip link set dev eth0 xdp obj /opt/xdp/xdp_ddos_kern.o sec xdp
ExecStop=/usr/sbin/ip link set dev eth0 xdp off
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now xdp-ddos.service
sudo systemctl status xdp-ddos.service --no-pager
Resolución de problemas comunes y próximos pasos
Algunas fallas recurrentes pueden resolverse con comprobaciones rápidas. Si nada carga, prueba el modo genérico, valida los headers del kernel y haz una recompilación limpia. Combina XDP con capas adicionales, como firewall L7 y límites de tasa aguas arriba. Para bloquear intentos de login, usa fail2ban con webhooks.
- ip link: "Operation not supported": driver sin XDP nativo — usa xdpgeneric.
- bpftool: "map not found": confirma el adjunto y el nombre del mapa.
- Sin contadores: genera tráfico o inserta una IP en blocked_ips.
- Perdiste acceso remoto: desadjunta vía consola: ip link set dev IFACE xdp off.
- Objetivo HTTP/3: si usas QUIC, desactiva DROP_UDP_TO_HTTP y recompila.
# Remover o programa XDP em caso de emergência
sudo ip link set dev "$IFACE" xdp off || sudo ip link set dev "$IFACE" xdpgeneric off
Preguntas frecuentes
¿eBPF/XDP sustituye un servicio anti-DDoS del operador o CDN?
No. XDP mitiga en el host con latencia mínima, filtrando antes de la pila de red. Esto ayuda contra tráfico malformado, spoofeado y UDP no deseado, pero no supera la capacidad del enlace. Para ataques volumétricos se necesita mitigación aguas arriba.
¿Puedo usar XDP si mi tarjeta de red no soporta el modo del driver?
Sí. Usa el modo genérico (skb), que se ejecuta tras la recepción del paquete y tiene mayor overhead. El comando ip link ofrece la opción “xdpgeneric”. Si el nativo falla, el genérico es un fallback funcional para VPS.
¿Qué ataques ayuda a mitigar este ejemplo?
Bloquea fuentes spoofeadas comunes (RFC1918, loopback, multicast y reservadas), descarta fragmentos IP y, opcionalmente, UDP hacia puertos HTTP. Es útil contra floods básicos. Puedes extender la lógica con límites por IP y blocklists externas.
¿Cómo monitorizar los contadores del XDP en producción?
Usa bpftool para leer mapas periódicamente y exportarlos a tu sistema de métricas. Una opción simple es recopilar con un script y enviarlos a un exporter. Para visualización, consulta la guía de Prometheus y Grafana en VPS.
¿Es seguro bloquear UDP en el puerto 443?
Solo durante un ataque y si no usas QUIC o HTTP/3. Bloquear UDP/443 interrumpe QUIC. Si tus clientes usan HTTP/3, desactiva la macro correspondiente en el código, recompila y readjunta el programa.
¿Necesitas adaptar XDP a tu tráfico real?
Si quieres ayuda para ajustar reglas, integrar métricas y automatizar la blocklist durante incidentes, podemos hablar sobre tu entorno y objetivos.
Hablar sobre mitigación con XDPVer también
- Tutorial: configurar mTLS entre Traefik y servicios en Docker Compose usando smallstep CA y certificados automáticos — paso a paso
- Tutorial: integrar Sentry en el pipeline de deploy con GitHub Actions para subir source maps automáticamente y generar issues (PHP y JavaScript) — paso a paso
- Configurar retención y compresión de logs de contenedores Docker con journald + logrotate + rsyslog en VPS con poco disco
- Tutorial: configurar GitHub Actions para tests de integración con PostgreSQL en contenedores efímeros y rollback automático de migrations — guía práctica