eBPF de cero a Cilium: cómo el kernel aprendió a saltarse su propia pila TCP/IP
TL;DR
eBPF es una máquina virtual sandboxed dentro del kernel Linux que ejecuta código verificado en hooks bien definidos: kprobes, tracepoints, socket events, drivers de red. Antes de eBPF, modificar el comportamiento del kernel era recompilarlo o cargar un módulo arbitrario; con eBPF, cargas un programa pequeño que pasa un verificador formal y se ejecuta a velocidad nativa con seguridad de memoria. En networking, esto se traduce en que el paquete no tiene que recorrer la pila TCP/IP tradicional: un programa eBPF en el driver de la NIC (XDP) puede dropear, reenviar o reescribir el paquete antes de que el kernel haya hecho su primer alloc; un programa en cgroup hooks (sock_ops) puede redirigir conexiones a otro socket sin que el paquete llegue siquiera a salir de la máquina. Cilium es el CNI que ha llevado esto a su conclusión lógica: reemplaza kube-proxy por eBPF puro (O(1) en lugar de O(N) de iptables), enruta pod-a-pod sin VXLAN cuando puede, evalúa Network Policies con BPF maps, y desde 1.16 ha rehecho su control plane de BGP con un set nuevo de CRDs —CiliumBGPClusterConfig, CiliumBGPPeerConfig, CiliumBGPAdvertisement, CiliumBGPNodeConfigOverride— que sustituyen al monolítico CiliumBGPPeeringPolicy que ya está deprecado. Este artículo baja por las tres capas (eBPF básico → eBPF networking → Cilium) y termina con los CRDs operativos.
La analogía: plugins firmados para el kernel
Imagina el navegador. Hace 20 años, extender un navegador significaba compilar un binario nativo y cargarlo: cualquier extensión podía estrellarlo, corromper memoria, leer cookies del banco. Hoy, las extensiones son JavaScript en una sandbox con un manifest que declara permisos, un runtime que aplica el aislamiento y una tienda que firma el código. La extensión no toca el binario del navegador; vive en un mundo controlado y solo puede hablar con el navegador a través de APIs definidas. Resultado: extensibilidad masiva con superficie de ataque acotada.
eBPF es exactamente eso para el kernel Linux. Cargar un módulo .ko clásico es cargar código nativo con acceso total a memoria del kernel: un bug y se va el sistema. eBPF es una VM bytecode con verificador estático, allocator controlado, JIT al hardware nativo después de pasar el verificador, y un set de “helpers” del kernel a los que puede llamar. El programa eBPF puede leer la memoria del kernel donde el verificador le permite leer, y solo eso. No puede entrar en loops infinitos (el verificador exige que termine). No puede saltar a direcciones arbitrarias. No puede dereferenciar punteros sin haberlos validado primero. Y, lo más importante: es código de usuario, cargado en runtime, que ejecuta dentro del kernel a velocidad nativa.
Las consecuencias se notan a kilómetros. Antes, observar tráfico en producción significaba parchear el kernel o cargar un módulo de riesgo. Hoy, bpftrace -e 'tracepoint:net:net_dev_xmit { @[args->dev->name] = count(); }' te da un histograma de paquetes por interfaz en tres líneas y cero downtime. Antes, sustituir iptables por algo más rápido implicaba reescribir el subsistema de netfilter. Hoy, Cilium carga 60 KB de bytecode eBPF en XDP y desbanca a iptables con un map de hashes.
eBPF básico: qué es y qué no es
El origen y el alcance
El nombre viene de Berkeley Packet Filter, una idea de 1992 (McCanne y Jacobson) para filtrar paquetes con un mini-bytecode que tcpdump usaba internamente. En 2014, Alexei Starovoitov lo rebautizó como eBPF y lo extendió enormemente: 11 registros de 64 bits en lugar de 2 de 32, stack de 512 bytes, mapas como estructuras compartidas con userspace, JIT al hardware nativo, y un verificador formal mucho más sofisticado. De ser un filtro de paquetes, pasó a ser un mecanismo de extensibilidad genérico del kernel.
Hoy eBPF se usa para cuatro cosas:
- Networking: XDP, TC, cgroup hooks, socket ops, lightweight tunnels.
- Observabilidad: kprobes, uprobes, tracepoints, USDT. La base de proyectos como
bpftrace,bcc, Pixie, Parca. - Seguridad: BPF LSM (Linux Security Module en eBPF), bloqueos de syscall con seccomp-bpf. Falco, Tetragon, Tracee.
- Scheduling: sched_ext (kernel 6.12+), schedulers de procesos completamente en eBPF. Aún en fase muy temprana.
La VM
Un programa eBPF se compila desde C (o Rust, Go con cilium/ebpf) a bytecode eBPF, no a x86/arm64 directamente. El loader del kernel (vía syscall bpf()) pasa ese bytecode por el verificador:
- Reconstruye el grafo de control de flujo.
- Hace análisis estático de cada path posible: cada instrucción tiene que ser alcanzable, cada acceso a memoria tiene que estar dentro de bounds conocidos, cada puntero tiene que haber sido validado.
- Rechaza loops sin un upper bound conocido. Los kernels recientes admiten loops acotados (
bpf_loophelper), pero el contador siempre es finito. - Rechaza llamadas a helpers o kfuncs que el program type del hook no permita.
Si el verificador acepta el programa, el JIT lo traduce a código nativo del host (x86, arm64, etc.) y queda atado a su hook. A partir de ahí se ejecuta cada vez que el evento del hook ocurre, sin context switch a userspace, sin coste de syscall. Latencias del orden de cientos de nanosegundos por invocación.
Maps: el puente con userspace
Un programa eBPF aislado no sirve de mucho. Lo que lo hace útil son los maps: estructuras de datos compartidas entre el programa kernel y el espacio de usuario. Hay varios tipos:
BPF_MAP_TYPE_HASH,BPF_MAP_TYPE_LRU_HASH: hash tables con o sin desalojo LRU.BPF_MAP_TYPE_ARRAY,BPF_MAP_TYPE_PERCPU_ARRAY: arrays, opcionalmente per-CPU para evitar contención.BPF_MAP_TYPE_RINGBUF,BPF_MAP_TYPE_PERF_EVENT_ARRAY: canales para enviar eventos a userspace en streaming.BPF_MAP_TYPE_PROG_ARRAY: arrays de programas eBPF para tail calls (encadenamiento de programas sin volver al kernel base).
Userspace lee y escribe estos maps vía syscalls bpf(); el programa kernel los lee y escribe directamente. Es la base de cualquier sistema eBPF: el programa kernel recoge datos en un map, el daemon en userspace los lee. Cilium hace exactamente esto: el agente userland (Go) gestiona la política y la traduce a entradas en maps; los programas eBPF que viven en XDP/TC leen los maps y aplican las decisiones.
CO-RE: compila una vez, corre en cualquier kernel
Una pesadilla clásica de los módulos de kernel: están atados a la versión exacta del kernel donde se compilaron. Distribuir un módulo precompilado para un parque de máquinas con distintas distros era imposible.
eBPF resuelve esto con CO-RE (Compile Once, Run Everywhere): el bytecode incluye relocaciones que el loader resuelve en cada kernel concreto consultando BTF (BPF Type Format), una representación del layout de las structs del kernel que el propio kernel publica. Resultado: un único binario eBPF funciona en kernels 5.10, 5.15, 6.1 y 6.8 sin recompilar, porque el loader ajusta los offsets de acceso a structs en runtime.
Esto es lo que ha permitido que distribuciones eBPF productivas existan. Sin CO-RE, cada kernel sería un proyecto de portado.
eBPF en networking: los hooks que importan
Dentro del subsistema de red de Linux, eBPF tiene varios hooks. Los relevantes para CNIs:
XDP — eXpress Data Path
XDP es el hook más temprano: se ejecuta en el driver de la NIC, antes de que el paquete entre en el kernel propiamente. No hay sk_buff (la struct que el resto del kernel usa para representar paquetes); solo hay un puntero a un buffer de RAM con los bytes recibidos.
Las acciones que un programa XDP puede devolver:
XDP_DROP: tirar el paquete inmediatamente. El driver lo deja caer y libera el buffer. Coste: nanosegundos. Caso de uso: DDoS mitigation. Cloudflare procesó más de 8 millones de paquetes/segundo por CPU con XDP para drop de SYN floods.XDP_PASS: dejar que el paquete siga al kernel normal. Pasa ask_buffy entra en el stack tradicional.XDP_TX: reenviar por la misma interfaz tras posibles modificaciones. Útil para load balancers L4 que reescriben destino y devuelven.XDP_REDIRECT: enviar el paquete a otra interfaz o a un map (para forward a userspace via AF_XDP, o a otra NIC, o a un veth de un pod).XDP_ABORTED: error (incrementa un contador, dropea).
Casos de uso reales:
- Cloudflare L3 DDoS protection: reglas XDP que drop millones de paquetes/s.
- Facebook Katran: L4 load balancer que reescribe destino IP y devuelve por la misma interfaz. Maneja 10× más conexiones por servidor que IPVS clásico.
- Cilium XDP acceleration: load balancing de Services en la capa más baja posible.
TC (Traffic Control) — clsact con BPF
XDP es muy rápido pero limitado: el paquete aún no tiene sk_buff y muchas decisiones (conntrack, NAT, encapsulación con metadatos) son más fáciles cuando sí lo tiene. El hook TC clsact con BPF se ejecuta después de construir el sk_buff pero antes de las decisiones de routing y netfilter. Acciones:
TC_ACT_OK: el paquete sigue por el stack.TC_ACT_SHOT: drop.TC_ACT_REDIRECT: redirigir a otra interfaz.TC_ACT_PIPE,TC_ACT_STOLEN: control de pipeline para combinarse con otros qdiscs.
Casos de uso:
- Network policy stateful: Cilium evalúa políticas L3-L7 en TC con
sk_buffcompleto y conntrack disponible. - Marcado y QoS: marcado de tráfico para que el scheduler aplique prioridades.
- Encapsulación overlay: añadir headers VXLAN/Geneve cuando el modo es tunnel.
XDP y TC se combinan: XDP para lo barato y temprano (DDoS, LB simple), TC para lo que necesita skb y stateful.
Cgroup hooks: sock_ops y CGROUP_SOCK_ADDR
El paso conceptual más radical: hooks que no están en la capa de red sino en la capa de socket. Tipos relevantes:
BPF_PROG_TYPE_CGROUP_SOCK_ADDR: se invoca cuando un proceso de un cgroup haceconnect(),bind(),sendto(). El programa eBPF puede reescribir la dirección de destino antes de que la conexión salga. Es lo que permite a Cilium hacer load balancing de Services sin que el paquete entre en la pila de red: si el cliente intenta conectar a10.96.0.1:443(ClusterIP), un programa eBPF en este hook reescribe el destino a la IP real del pod backend antes de que el syscall continúe.BPF_PROG_TYPE_SOCK_OPS: se invoca en eventos TCP (creación, establecido, retransmisión). Permite ajustar parámetros del socket en runtime y, lo más importante, emparejar sockets locales víabpf_sk_assignpara shortcut sin que el paquete viaje por la red.
Esta es la “tercera capa” del bypass: no es solo más rápido, es conceptualmente distinto. El paquete no se construye, no se serializa, no recorre IP layer ni TCP layer. Es la diferencia entre acelerar una carretera y descubrir que para algunos viajes no hace falta tomar el coche.
El camino largo: cómo es la pila TCP/IP tradicional
Para apreciar lo que eBPF ahorra, vale la pena trazar el recorrido completo de un paquete por la pila Linux. Tomemos el caso “paquete entra por una NIC, va a un proceso local”:
NIC (DMA al ring buffer del driver)
↓
driver: napi_schedule, poll, asigna sk_buff
↓
[XDP hook] ← si hay programa XDP, se decide aquí
↓
netif_receive_skb
↓
__netif_receive_skb_core
↓
[TC ingress clsact + BPF] ← si hay programa TC ingress
↓
packet_type handlers (IP, ARP...)
↓
ip_rcv → ip_rcv_core
↓
[netfilter NF_INET_PRE_ROUTING] ← iptables PREROUTING
↓
routing decision (FIB lookup)
↓
[netfilter NF_INET_LOCAL_IN] o [NF_INET_FORWARD]
↓
tcp_v4_rcv → tcp_v4_do_rcv
↓
tcp_rcv_established
↓
sk_data_ready
↓
proceso lee con recv()/read()
Cada flecha es una llamada de función con coste medible. Cada netfilter hook recorre todas las reglas iptables/nftables registradas. Con kube-proxy en modo iptables y 5 000 Services × 10 endpoints cada uno, hay del orden de 150 000 reglas que se evalúan secuencialmente en NF_INET_PRE_ROUTING. Los benchmarks publicados muestran latencias de decenas de microsegundos por paquete en clusters Kubernetes grandes solo en el paso de netfilter, antes de que la aplicación reciba nada.
Y eso es el camino normal. En el camino de salida pasa lo mismo pero en sentido inverso: tcp_sendmsg → ip_output → NF_INET_LOCAL_OUT → routing → NF_INET_POSTROUTING → dev_queue_xmit → driver → NIC.
Cómo Cilium se salta esta pila
Cilium no elimina la pila TCP/IP; sigue ahí para los casos que la necesitan. Lo que hace es shortcuts en los puntos donde duele.
Shortcut 1 — XDP para el datapath de Service
Para un cluster con 5 000 Services, kube-proxy iptables tiene un coste O(N) en evaluar reglas (incluso con iptables-restore --noflush y trucos, sigue siendo lineal en el número de chains que el paquete atraviesa).
Cilium lo sustituye así:
- Cada Service y sus endpoints viven en un eBPF hash map.
- Cuando entra un paquete con destino a una ClusterIP, el programa XDP de Cilium hace un lookup O(1) en ese map y obtiene el endpoint backend.
- Reescribe el destino y
XDP_TX(devuelve por la misma interfaz hacia el backend) oXDP_REDIRECT(lo envía a la veth del pod local correspondiente).
Esto significa que el coste no crece con el número de Services. 100 Services o 100 000, lookup constante en el map. Benchmarks publicados muestran reducciones de latencia de 30-50% en clusters con muchos Services frente a kube-proxy iptables, y de orden de magnitud frente a IPVS en algunos casos.
Shortcut 2 — socket-LB: el paquete no se llega a construir
Cilium 1.6+ introdujo el socket-level load balancing, basado en cgroup hooks. Funciona así:
- Cuando un pod hace
connect(10.96.0.1:443)(ClusterIP de un Service), el syscall entra en el kernel. - Antes de que el kernel construya nada de red, un programa eBPF en
CGROUP_SOCK_ADDR/connect4intercepta y reescribe la dirección de destino a la IP real del pod backend. - El kernel continúa con el
connectcomo si el cliente hubiera escrito directamente10.0.0.42:8080.
¿Por qué importa? Porque cuando el pod backend está en el mismo nodo, este shortcut convierte una llamada que habría implicado:
syscall connect → kernel stack → veth → bridge → veth → kernel stack → syscall accept
en:
syscall connect (con destino reescrito) → loopback directo
La pila TCP/IP se evita literalmente. No hay paquete encapsulado, no hay viaje por veth pairs, no hay netfilter. Latencias L7 de pod-a-pod en el mismo nodo bajan a niveles de comunicación local (~5-15 µs en lugar de ~30-50 µs para servicios con kube-proxy iptables y veth tradicional).
Shortcut 3 — direct routing pod-a-pod
El modo overlay tradicional (Flannel, Calico VXLAN) encapsula cada paquete pod-a-pod en VXLAN/Geneve. Cada paquete carga un header extra de 50 bytes, requiere encap/decap, y consume MTU.
Cilium soporta direct routing: los pod CIDRs se anuncian a la fabric subyacente (con BGP, ahí entra el control plane que veremos) y los routers físicos enrutan los paquetes pod-a-pod sin encapsular. El paquete sale de un pod con su IP original como source y la IP del pod destino como dest, la NIC del nodo lo entrega a la red, la red lo enruta, llega al nodo destino y se entrega al pod. Cero encap, MTU completo, latencia mínima.
Cilium hace esto vía programas eBPF en TC que reescriben las cabeceras necesarias y deciden si el paquete va por encap o direct según la política configurada por nodo.
Shortcut 4 — Network Policy en TC con maps
Las Network Policies en CNIs clásicos suelen traducirse a reglas iptables, otro factor que explota linealmente. Cilium las evalúa en programas eBPF que leen maps de identity: cada workload tiene un identifier numérico calculado desde sus labels, y la policy es un map (src_identity, dst_identity, port, proto) → allow|deny. Un lookup en hash map por paquete.
Esto también permite las L7 policies de Cilium (filtrado HTTP, gRPC, Kafka): el programa eBPF reconoce el handshake L7, redirige selectivamente al proxy Envoy embebido (que vive como sidecar del datapath, no como sidecar de pod) y solo en ese subset paga el coste del proxy L7. Todo el tráfico L3/L4 sigue por el fast path eBPF.
Cilium: la arquitectura
Cilium combina dos planos:
- Agent (Go): vive como DaemonSet en cada nodo. Es lo “lento”: traduce el deseo expresado en CRDs (CiliumNetworkPolicy, CiliumBGPClusterConfig, etc.) en entradas en eBPF maps. Habla con el API server de Kubernetes para descubrir endpoints, services, pods. Embebe un GoBGP para el control plane de BGP. Embebe un Envoy para L7 policies.
- Datapath (eBPF): los programas cargados en XDP, TC, cgroup hooks. Son lo “rápido”: ven cada paquete, leen los maps que el agent mantiene, deciden en nanosegundos.
Esta separación es lo que hace operacionalmente cómodo a Cilium: el deseo se expresa en YAML, el agent lo materializa en maps, los maps son leídos por el datapath. Si el agent se cae temporalmente, el datapath sigue funcionando con la última configuración cargada. Como en cualquier sistema de control/data plane bien hecho.
El BGP Control Plane v2: los CRDs que tienes que conocer
Cilium ha tenido soporte de BGP varios años. La primera versión usaba un único CRD monolítico, CiliumBGPPeeringPolicy, que mezclaba en un solo objeto la configuración del nodo, los peers, los timers y los anuncios. Desde Cilium 1.16 existe el BGP Control Plane v2, que descompone esa configuración en CRDs separados con responsabilidades claras. El CiliumBGPPeeringPolicy (API cilium.io/v2alpha1) está deprecated y los avisos de migración aparecen en los logs del operator si todavía lo usas.
Los CRDs nuevos (API cilium.io/v2):
1. CiliumBGPClusterConfig
Define instancias BGP y los peers a los que se conectan, desde la perspectiva del cluster. Se selecciona qué nodos aplican esta configuración con un nodeSelector.
apiVersion: cilium.io/v2
kind: CiliumBGPClusterConfig
metadata:
name: cilium-bgp-cluster
spec:
nodeSelector:
matchLabels:
bgp-policy: rack-1 # solo nodos con esta label
bgpInstances:
- name: instance-65000
localASN: 65000
peers:
- name: top-of-rack-1a
peerASN: 64512
peerAddress: 10.0.1.1
peerConfigRef:
name: tor-shared-config # → referencia a CiliumBGPPeerConfig
- name: top-of-rack-1b
peerASN: 64512
peerAddress: 10.0.1.2
peerConfigRef:
name: tor-shared-config
Una instancia BGP es la abstracción “este nodo participa en BGP con este ASN local y estos peers”. Pueden coexistir varias en un mismo nodo (multi-instancia para multi-VRF).
2. CiliumBGPPeerConfig
Define los parámetros compartidos del peering: timers, address families, transport, password MD5, graceful restart, etc. Se referencia desde CiliumBGPClusterConfig via peerConfigRef. Esto evita repetir la misma configuración para cada peer cuando hay decenas de ellos.
apiVersion: cilium.io/v2
kind: CiliumBGPPeerConfig
metadata:
name: tor-shared-config
spec:
timers:
holdTimeSeconds: 30
keepAliveTimeSeconds: 10
connectRetryTimeSeconds: 5
gracefulRestart:
enabled: true
restartTimeSeconds: 120
families:
- afi: ipv4
safi: unicast
advertisements:
matchLabels:
advertise: bgp # → liga a CiliumBGPAdvertisement
- afi: ipv6
safi: unicast
advertisements:
matchLabels:
advertise: bgp
authentication:
password:
name: bgp-md5-secret # Secret con la password MD5
key: password
Una sola CiliumBGPPeerConfig puede ser referenciada por muchos peers distintos. Cambias timers o families en un sitio.
3. CiliumBGPAdvertisement
Declara qué prefijos se anuncian: los pod CIDRs del nodo, los ClusterIPs y ExternalIPs de Services, las IPs asignadas por CiliumLoadBalancerIPPool para Services type=LoadBalancer. Se vinculan a CiliumBGPPeerConfig via labels.
apiVersion: cilium.io/v2
kind: CiliumBGPAdvertisement
metadata:
name: services-and-pods
labels:
advertise: bgp # ← la label que el PeerConfig matchea
spec:
advertisements:
- advertisementType: PodCIDR # anuncia el pod CIDR del nodo
attributes:
communities:
standard:
- "65000:100"
- advertisementType: Service # anuncia ClusterIPs / LoadBalancer IPs
service:
addresses:
- LoadBalancerIP
- ClusterIP
- ExternalIP
selector:
matchLabels:
bgp-advertise: "true" # solo Services con esta label
attributes:
communities:
standard:
- "65000:200"
localPreference: 200
La granularidad es muy fina: puedes anunciar diferentes tipos de prefijos con diferentes communities BGP, diferentes local-preference, diferentes path attributes, y filtrar Services con label selectors. Esto era literalmente imposible con CiliumBGPPeeringPolicy v1.
4. CiliumBGPNodeConfig (auto-generado)
Este CRD no se configura a mano. El operator de Cilium lo genera por cada nodo a partir de la CiliumBGPClusterConfig que aplica a ese nodo. Es el estado materializado per-node que el agente de cada nodo lee para arrancar sus peerings. Si quieres ver qué configuración BGP está corriendo realmente en un nodo, kubectl get ciliumbgpnodeconfig <nodename> -o yaml te lo enseña.
5. CiliumBGPNodeConfigOverride
Opcional. Permite sobrescribir la configuración generada para un nodo concreto cuando necesitas algo no-estándar. Casos de uso:
- Anclar el router-id BGP a una IP específica (útil cuando el nodo tiene varias interfaces).
- Especificar la dirección local del peer cuando hay varias interfaces de salida.
- Cambiar timers solo para un nodo problemático.
apiVersion: cilium.io/v2
kind: CiliumBGPNodeConfigOverride
metadata:
name: node-rack1-master01 # nombre debe coincidir con el del nodo
spec:
bgpInstances:
- name: instance-65000
routerID: 10.0.1.10 # override del router-id
peers:
- name: top-of-rack-1a
localAddress: 10.0.1.10 # interface local concreta
Diagrama de relaciones
CiliumLoadBalancerIPPool: complementa, no es BGP
Aunque no es un CRD BGP estrictamente, conviene mencionarlo: CiliumLoadBalancerIPPool es el CRD que provee IPs a Services type=LoadBalancer. Define un rango (10.20.0.0/24, por ejemplo) que Cilium asigna automáticamente a Services LoadBalancer. Combinado con CiliumBGPAdvertisement que anuncia LoadBalancerIP, da el ciclo completo: Service nuevo → IP asignada del pool → anuncio BGP a los routers → IP routable desde la red corporativa, sin balanceador externo.
apiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: lb-pool-rack1
spec:
blocks:
- start: "10.20.0.10"
stop: "10.20.0.250"
serviceSelector:
matchLabels:
lb-pool: rack1
Manifest completo: pod CIDRs + LoadBalancer Services anunciados a un par ToR redundante
Ejemplo realista de un cluster con dos top-of-rack switches como peers BGP, ambos en el mismo AS (64512), Cilium en AS 65000:
# 1. CiliumBGPPeerConfig — config compartida para los dos ToR
---
apiVersion: cilium.io/v2
kind: CiliumBGPPeerConfig
metadata:
name: tor-peers
spec:
timers:
holdTimeSeconds: 30
keepAliveTimeSeconds: 10
gracefulRestart:
enabled: true
restartTimeSeconds: 120
families:
- afi: ipv4
safi: unicast
advertisements:
matchLabels:
advertise: bgp
# 2. CiliumBGPAdvertisement — qué se anuncia
---
apiVersion: cilium.io/v2
kind: CiliumBGPAdvertisement
metadata:
name: pods-and-lb
labels:
advertise: bgp
spec:
advertisements:
- advertisementType: PodCIDR
- advertisementType: Service
service:
addresses: [LoadBalancerIP]
selector:
matchExpressions:
- { key: io.kubernetes.service.namespace, operator: NotIn, values: [kube-system] }
# 3. CiliumBGPClusterConfig — qué nodos hablan con qué peers
---
apiVersion: cilium.io/v2
kind: CiliumBGPClusterConfig
metadata:
name: cluster-bgp
spec:
nodeSelector:
matchLabels:
bgp: enabled
bgpInstances:
- name: instance-65000
localASN: 65000
peers:
- name: tor-a
peerASN: 64512
peerAddress: 10.0.1.1
peerConfigRef: { name: tor-peers }
- name: tor-b
peerASN: 64512
peerAddress: 10.0.1.2
peerConfigRef: { name: tor-peers }
# 4. CiliumLoadBalancerIPPool — rango de LB IPs
---
apiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: lb-corporate
spec:
blocks:
- cidr: "10.20.0.0/24"
Cuatro objetos. Antes, en v1, era un único CiliumBGPPeeringPolicy que mezclaba todo y resultaba difícil de mantener en clusters de más de 5 nodos con configuración heterogénea. La nueva separación es más larga pero claramente factorizable: un PeerConfig por tipo de peer, una Advertisement por política de anuncio, un ClusterConfig que conecta nodos con peers.
Trampas operativas comunes
Modo routingMode: tunnel con BGP
BGP solo tiene sentido con direct routing (routingMode: native). Si tienes el modo tunnel (VXLAN/Geneve) y configuras BGP, anunciarás pod CIDRs pero los paquetes seguirán saliendo encapsulados, generando un comportamiento confuso (a veces vía túnel, a veces directo según rutas). Configura routingMode: native y desactiva túnel.
eBPF host routing vs kube-proxy replacement
Son dos cosas distintas. kubeProxyReplacement: true habilita el reemplazo de kube-proxy (los Services). bpf.hostRouting: true habilita el bypass de iptables del host (las decisiones de routing del nodo se hacen con eBPF en lugar de FIB tradicional). El segundo necesita kernel 5.10+ con todas las features bpf habilitadas; si no tienes ese kernel, queda en modo legacy y el rendimiento es solo “casi tan bueno”.
BGP timers agresivos sobre NICs flapping
Con holdTimeSeconds: 9 / keepAliveSeconds: 3, una NIC que parpadee 5 segundos rompe la sesión BGP y todas las rutas anunciadas desaparecen del fabric. Los pods de ese nodo se vuelven inalcanzables hasta que la sesión se restablece. Para clusters en hardware con NICs sospechosas, usa los valores conservadores (holdTime: 30, keepAlive: 10) y considera graceful restart explícitamente (ya viene en el ejemplo de arriba).
Anunciar ClusterIP a la red corporativa
Anunciar ClusterIP a routers externos es rara vez lo que quieres: son IPs de Service interno, no diseñadas para alcanzarse desde fuera del cluster. Para exposición externa, usa LoadBalancerIP desde un CiliumLoadBalancerIPPool. Anunciar ClusterIP solo tiene sentido en topologías muy específicas (multi-cluster mesh con shared service discovery).
Mezclar v2alpha1 (CiliumBGPPeeringPolicy) y v2 (CiliumBGPClusterConfig)
No funciona bien. El operator emite warnings en los logs sobre el uso de la API deprecated, y los conflictos entre lo que define la peering policy vs la cluster config pueden producir estados raros. Migra de una a la otra en una sola pasada; no convivas.
MD5 password y MTU
Si configuras MD5 password en CiliumBGPPeerConfig.authentication, el header TCP es más grande. En links con MTU justa (1500 - 50 VXLAN del fabric upstream, por ejemplo), el handshake BGP puede fragmentar y morir silenciosamente. O usa MTU 9000 entre nodos y ToR, o asegura que los MSS están negociados correctamente.
Lo que no hemos cubierto
- Cilium Cluster Mesh: federación de varios clusters Cilium para que sus Services se vean entre sí. Encaja con BGP cuando se quiere routing nativo entre clusters; tiene CRDs propios.
- L7 Policies y Envoy embebido: política HTTP/gRPC/Kafka. Otra capa de eBPF + proxy que merece artículo propio.
- Hubble: observabilidad de tráfico basada en eBPF que Cilium expone. Dashboards de flow logs con cero impacto en latencia.
- Wireguard transparente: encryption pod-a-pod sin sidecars, controlado por Cilium via eBPF redirect a un dataplane WireGuard del kernel.
- Gateway API en Cilium: el sucesor de Ingress, con soporte de primera clase desde Cilium 1.16+.
- eBPF para LLM serving: la conexión natural con la serie anterior de inferencia. Hay trabajos recientes que usan eBPF para fairness multi-tenant en GPUs y para tracking de tokens; territorio de papers, aún no production.
Es decir, queda material para otros tres artículos de la serie de hoy. Vamos en orden.
Referencias
Conceptuales y de proyecto:
- eBPF.io — documentación canónica del ecosistema eBPF.
- The BPF Compiler Collection (bcc) y bpftrace — herramientas para empezar.
- eBPF en 2026: How Extended Berkeley Packet Filter Became the Engine of Linux Observability and Networking — estado del arte.
XDP, TC y firewalling:
- Getting started with XDP and eBPF (Red Hat docs).
- Full Guide to BPF Firewalls: XDP, tc, and eBPF Integration (Medium, 2025).
- Cloudflare blog: XDP for DDoS mitigation.
- Facebook Katran (GitHub) — L4 LB con XDP, código y paper.
Cilium:
- Cilium documentation — siempre el primer puerto.
- Kubernetes Without kube-proxy — la guía oficial del replacement.
- Cilium BGP Control Plane Resources (docs) — referencia de los CRDs v2.
- Configuring Cilium BGP Control Plane (OneUptime blog, mar 2026) — walkthrough.
- A Guide to BGP Control Plane and Cluster Mesh in Cilium Networking (Sigrid Jin, Medium) — artículo más profundo con casos de uso.
Cross-references:
- Artículo previo en este blog: Kubernetes con Cilium BGP: servicios accesibles sin Ingress — el primer paso, con la versión v1 (que ya hay que migrar).
- Series sobre inferencia LLM: KV cache, vLLM en Kubernetes, PagedAttention deep dive, Operators LLM K8s — donde la red rápida (que veremos en los siguientes posts de esta serie) determina el rendimiento real.