wandres.dev
NAPI, SOCKETS Y XDP · recepción, protocolos, fast path

XDP en producción: firewalls DDoS, balanceadores y observabilidad

Cómo XDP y eBPF sostienen en 2026 firewalls anti-DDoS a velocidad de línea que descartan con XDP_DROP consultando mapas, balanceadores L4 como Katran de Meta con hashing consistente y bpf_redirect_map, y observabilidad sin copiar un solo paquete a usuario; los mapas eBPF como sistema nervioso entre plano de datos y plano de control, y AF_XDP como puente al espacio de usuario.

⏱ 18 min

Todo lo aprendido en el nivel converge aquí, en máquinas reales que absorben terabits de ataque y balancean el tráfico de centros de datos enteros sin un solo aparato propietario. Un firewall que tira basura a velocidad de línea, un balanceador que reparte millones de conexiones sin estado por conexión, un observatorio que ve cada paquete sin copiar ninguno: los tres son el mismo patrón —un programa XDP por paquete y un mapa eBPF compartido con el espacio de usuario— aplicado a tres problemas. Esta lección es el destino natural de NAPI, los sockets y XDP: el plano de datos programable, en el kernel, seguro y a velocidad de línea.

🎯 Al terminar esta lección sabrás
  • Construir un firewall anti-DDoS que descarta con XDP_DROP consultando un mapa.
  • Entender un balanceador L4 tipo Katran: hash consistente, encapsulado y bpf_redirect_map.
  • Usar mapas eBPF para contar, bloquear y observar a velocidad de línea.
  • Ver AF_XDP y los mapas de redirección como puente hacia el espacio de usuario.

Firewall anti-DDoS a velocidad de línea

El caso canónico. Un programa XDP consulta un mapa de IPs bajo vigilancia, cuenta lo que ve y tira lo que supera un umbral, todo antes de reservar un sk_buff. El mapa LRU_HASH desaloja solo las entradas viejas cuando se llena —imprescindible bajo un ataque que inventa millones de IPs de origen—, y un PERCPU_ARRAY acumula contadores sin candados.

struct {
	__uint(type, BPF_MAP_TYPE_LRU_HASH);
	__type(key, __be32);          /* IP de origen */
	__type(value, __u64);         /* paquetes en la ventana */
	__uint(max_entries, 1000000);
} vistos SEC(".maps");

struct {
	__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
	__type(key, __u32);
	__type(value, __u64);
	__uint(max_entries, 2);       /* indice 0: pasados, indice 1: tirados */
} stats SEC(".maps");

SEC("xdp")
int xdp_ddos(struct xdp_md *ctx)
{
	void *data = (void *)(long)ctx->data, *end = (void *)(long)ctx->data_end;
	struct ethhdr *eth = data;

	if ((void *)(eth + 1) > end)
		return XDP_DROP;
	if (eth->h_proto != bpf_htons(ETH_P_IP))
		return XDP_PASS;

	struct iphdr *ip = (void *)(eth + 1);
	if ((void *)(ip + 1) > end)
		return XDP_DROP;

	__u64 uno = 1, *n = bpf_map_lookup_elem(&vistos, &ip->saddr);
	if (n)
		__sync_fetch_and_add(n, 1);
	else
		bpf_map_update_elem(&vistos, &ip->saddr, &uno, BPF_ANY);

	__u32 idx = (n && *n > UMBRAL_PPS) ? 1 : 0;
	__u64 *c = bpf_map_lookup_elem(&stats, &idx);
	if (c)
		__sync_fetch_and_add(c, 1);
	return idx ? XDP_DROP : XDP_PASS;    /* a velocidad de linea, sin skb */
}

Cloudflare construyó exactamente esto para su mitigación de borde: XDP tira los paquetes hostiles antes de que cuesten nada, y por eso un servidor de propósito general puede sostener descartes que antes exigían hardware dedicado.

Balanceo de carga L4: la lección de Katran

Katran, el balanceador L4 de Meta, sustituyó a IPVS en el borde. Una IP virtual (VIP) recibe millones de conexiones; Katran elige un servidor real con hashing consistente de Maglev sobre la 5-tupla, de modo que el mismo flujo aterriza siempre en el mismo backend sin guardar estado por conexión, y añadir o quitar backends perturba un mínimo de flujos. Encapsula el paquete hacia el elegido y lo reenvía con bpf_redirect_map sobre un DEVMAP, todo en XDP, por núcleo, sin candados.

/* esqueleto conceptual del reenvio de un balanceador L4 */
struct backend *b = maglev_lookup(&vip_ring, hash_5tupla(ip, l4));
if (!b)
	return XDP_PASS;
if (encap_ipip(ctx, b->addr) < 0)     /* envuelve en IPIP hacia el backend */
	return XDP_DROP;
return bpf_redirect_map(&tx_devs, b->oif, 0);  /* sale por la NIC del backend */

La clave conceptual es la ausencia de tabla por conexión: la persistencia de flujo no la da un estado guardado sino una función pura sobre la 5-tupla, y por eso escala a decenas de millones de paquetes por segundo por anfitrión. Cilium lleva esta misma idea a los servicios de Kubernetes.

🛡️

Firewall DDoS

XDP_DROP con listas y contadores en mapas. Descarta lo hostil antes del sk_buff. Absorbe ataques volumétricos en hardware común.

⚖️

Balanceador L4

Hash consistente y bpf_redirect_map. Reparte sin estado por conexión. Es Katran, es Cilium: el borde y el clúster.

🔭

Observabilidad

Muestreo, conteo y captura por mapa, sin copiar paquetes a usuario. El plano de datos se instrumenta a sí mismo.

Observabilidad y mapas: el sistema nervioso de eBPF

Como el programa ve cada paquete, puede muestrear, contar y exportar métricas de flujo por mapas que un agente en usuario lee periódicamente —detectores de DDoS, contadores por VIP, histogramas de latencia, captura tipo xdpdump— sin copiar un solo paquete a usuario y sin perturbar el camino rápido. El mapa es la memoria compartida entre el plano de datos —kernel, por paquete— y el plano de control —usuario, periódico—.

# el plano de control lee por el sistema de ficheros de bpf, sin tocar el camino rapido
bpftool map dump name stats     # contadores de pasados y tirados, por CPU
bpftool prog show               # el programa XDP cargado y su JIT
ip -s link show dev eth0        # gancho xdp y contadores del driver
flowchart LR
X[Programa XDP por paquete] --> M[Mapa eBPF compartido]
A[Agente en usuario bpftool o daemon] --> M
X -->|DROP TX o REDIRECT| Red[Salida de red]
M -->|reglas y umbrales| X

Y cuando el trabajo debe correr en usuario a velocidad de línea, XDP_REDIRECT hacia un XSKMAP entrega el marco crudo a un socket AF_XDP mapeado sobre un anillo de memoria compartida (UMEM): procesamiento de paquetes en espacio de usuario de clase DPDK, pero conservando el kernel la propiedad de la NIC y el verificador la seguridad del clasificador. Es la vía de escape para cargas que no caben dentro del kernel.

ℹ️
El programa no reemplaza la pila: la precede

XDP no sustituye a la pila de red; se antepone a ella. Lo que devuelve XDP_PASS sigue el camino de siempre —sk_buff, sockets, tcp_recvmsg—, y ahí conviven las conexiones normales de la máquina con el plano rápido que filtra o desvía. Por eso un mismo servidor puede ser a la vez balanceador XDP y anfitrión de servicios: el gancho decide, paquete a paquete, qué merece la pila completa y qué se resuelve antes.

El plano de datos se volvió programable sin volverse inseguro

Cierra el arco y contempla lo que el nivel entero venía construyendo. NAPI enseñó al kernel a procesar por lotes bajo carga, midiendo su propia presión con el mismo gesto con que la atiende. La división socket/sock le enseñó a separar la interfaz estable del protocolo intercambiable. El sk_buff le enseñó a mover paquetes reanotando en vez de copiar. Y XDP dio el último paso: dejar que el operador inyecte su propia lógica de red en el punto más temprano y más caliente, demostrada segura por un verificador, compilada a nativo por un JIT, coordinada con el espacio de usuario a través de nada más que un mapa compartido. El resultado disuelve una dicotomía que dominó dos décadas de ingeniería de redes: ya no hay que elegir entre la pila del kernel, segura pero lenta, y el bypass de usuario tipo DPDK, rápido pero inseguro y ciego al resto del sistema. eBPF es una tercera cosa —dentro del kernel, a velocidad de línea y segura— y es la razón de que un solo servidor común pueda absorber un DDoS de terabits o balancear el tráfico de un centro de datos sin un aparato propietario. La enseñanza última de todo el track aterriza aquí, y no es sobre redes: el rendimiento en sistemas no es velocidad bruta, es rechazar trabajo temprano y mover la decisión al punto donde el objeto es más barato de razonar. Katran no procesa conexiones más rápido: se niega a materializarlas hasta el último instante. El firewall no filtra paquetes veloces: los tira antes de que sean paquetes. XDP es ese principio convertido en una instrucción que el operador puede escribir, cargar en caliente y observar por un mapa, sin recompilar el kernel ni cargar un módulo. Cuando lo interiorizas, dejas de ver la red como una tubería que atraviesas y empiezas a verla como una superficie programable donde tú decides, byte a byte y antes que nadie, qué existe y qué no.

⚔️ Del filtro al plano de datos
  1. Carga el programa anti-DDoS, genera tráfico desde una IP por encima del umbral con hping3 o pktgen, y observa crecer el contador de tirados en el mapa stats con bpftool map dump.
  2. Explica por qué un PERCPU_ARRAY para los contadores evita un candado y cómo el usuario suma los valores por CPU.
  3. Lee el código XDP de Katran o de Cilium y localiza el mapa de redirección y la búsqueda de hash consistente; argumenta por qué la persistencia de flujo no necesita tabla por conexión.
  4. Razona por qué LRU_HASH, y no un HASH simple, es el mapa correcto para una lista de bloqueo bajo ataque.
  5. Contrasta AF_XDP con DPDK: qué conserva el kernel, y qué seguridad sigue garantizando el verificador sobre el clasificador.