Escalando Ingress QUIC: Steering de Sockets con eBPF para Migración de Conexiones HTTP/3

Quick answer
Escalando HTTP/3 para Telemetría de Alta Frecuencia: Socket con eBPF: MCP tunnel answer
MCP tunneling gives a local MCP server a public HTTPS endpoint so AI tools can reach it during development without deploying the server first.
What is MCP tunneling?
MCP tunneling exposes a local Model Context Protocol server through a public endpoint so compatible AI tools can connect during development.
When should I use InstaTunnel for MCP?
Use InstaTunnel Pro when a local MCP endpoint needs public HTTPS access, stable routing, and stream-friendly tunnel behavior.
Cuando un nodo remoto en el borde se desconecta de la red durante unos cientos de milisegundos y vuelve con una nueva dirección IP, una implementación ingenua de proxy UDP terminará silenciosamente la sesión que debería sobrevivir a ese tipo de interrupción. Este artículo analiza por qué sucede eso y cómo el steering de sockets basado en eBPF en la capa del kernel lo soluciona — usando los mecanismos reales que Linux y Cloudflare implementan, no solo la teoría.
¿Por qué QUIC, y por qué rompe el balanceo de carga ingenuo?
La telemetría en tiempo real — redes de sensores industriales, fusión de sensores en vehículos autónomos, cargas de trabajo en edge móvil — ha migrado en gran medida de TCP a la transportación QUIC de HTTP/3. La entrega en orden estricta de TCP significa que un paquete perdido detiene cada flujo multiplexado en esa conexión (bloqueo head-of-line). QUIC evita esto gestionando su propia recuperación de pérdida y multiplexación de flujos directamente sobre UDP, por lo que un paquete perdido en un flujo no detiene los otros.
QUIC también soporta 0-RTT — pero vale ser precisos sobre qué significa eso: 0-RTT permite a un cliente que regresa reanudar una sesión previa y enviar datos de aplicación inmediatamente, usando una clave precompartida de un apretón de manos anterior. Un cliente completamente nuevo aún necesita un apretón de manos TLS 1.3 de 1-RTT completo; 0-RTT es una optimización de reanudación, no una propiedad de cada apretón de manos QUIC.
La característica más importante para este artículo es migración de conexión. Una conexión TCP está anclada a un 4-tuple — IP fuente, puerto fuente, IP destino, puerto destino. Cambiar cualquiera de estos (un teléfono que cambia de Wi-Fi a 5G, un robot que se desplaza entre puntos de acceso) hace que la conexión desaparezca; el cliente debe renegociar desde cero. QUIC desacopla la sesión del camino de red identificándola con un Connection ID (CID) en lugar del 4-tuple. Según RFC 9000, un CID puede tener hasta 20 bytes y es opaco para el par — el servidor lo elige, se lo entrega al cliente, y puede seguir reconociendo a ese cliente incluso después de que cambie su IP y puerto en medio de la sesión.
Eso es una gran ventaja para un solo cliente hablando con un solo servidor. Se vuelve un problema cuando el lado del servidor en realidad es una flota de procesos de trabajo balanceados.
El hash del 4-tuple se rompe bajo migración
Los proxies inversos como NGINX, Envoy y HAProxy escalan en múltiples núcleos de CPU ejecutando varios procesos de trabajo, cada uno con su propio socket ligado al mismo puerto mediante SO_REUSEPORT. Para TCP, esto es sencillo: el kernel maneja el apretón de manos y accept() entrega una conexión completa a exactamente un proceso, que el kernel seguirá enrutando durante toda la vida de esa conexión.
UDP no tiene apretón de manos ni estado de conexión persistente en el kernel, por lo que SO_REUSEPORT recurre a un mecanismo mucho más simple: para cada datagrama entrante, el kernel calcula un hash del 4-tuple y selecciona un socket del grupo reuseport mediante ese hash. Mientras el 4-tuple permanezca fijo, cada paquete llega al mismo proceso.
El instante en que la IP del cliente cambia — el objetivo principal de la migración de conexión en QUIC — el 4-tuple cambia, el hash cambia, y el kernel enrutará el paquete a un diferente proceso que nunca ha visto a ese cliente, no tiene claves TLS para él, y no tiene más opción que descartar el paquete. La característica principal de QUIC se neutraliza por un mecanismo de balanceo de carga que es anterior a ella.
Enseñando al kernel sobre QUIC con eBPF
En lugar de codificar a mano la conciencia de QUIC en el kernel, Linux permite adjuntar un programa eBPF personalizado a un grupo reuseport y dejar que tome la decisión de selección del socket en lugar del hash predeterminado. Esta capacidad es BPF_PROG_TYPE_SK_REUSEPORT, añadida por Martin KaFai Lau en Linux 4.19, y se combina con la función auxiliar bpf_sk_select_reuseport(), que asigna un paquete entrante a un socket específico en un mapa BPF_MAP_TYPE_REUSEPORT_SOCKARRAY (y, desde Linux 5.8, también en mapas SOCKHASH/SOCKMAP). Si el programa eBPF devuelve un índice inválido, el kernel recurre silenciosamente al hash del 4-tuple, por lo que el mecanismo degrada de forma segura.
Esto permite reemplazar “hash del 4-tuple” por “leer el CID de QUIC del paquete y enrutar en base a eso” — completamente en espacio kernel, antes de que el paquete llegue a un buffer de socket en espacio usuario.
La canalización de steering
- El worker incrusta su identidad en el CID. Durante el primer paquete de apretón de manos, antes de que ocurra cualquier migración, el hash predeterminado es inofensivo — no hay estado establecido aún para enrutar mal. El worker que realiza el apretón (por ejemplo, Worker 2) genera el Server Connection ID que devuelve al cliente, y codifica su propio índice de worker en algún lugar de esos bytes junto con entropía criptográfica.
- El programa eBPF analiza el encabezado QUIC en kernel. En cada paquete posterior, el programa
sk_reuseportinspecciona la carga útil en bruto mediantestruct sk_reuseport_md, distingue el encabezado largo de QUIC (paquetes de apretón de manos) del encabezado corto (paquetes de estado estable 1-RTT), y extrae el campo Destination Connection ID. - Búsqueda del ID del worker, no un escaneo en tabla hash. Debido a que el ID del worker está incrustado directamente en el CID en lugar de requerir una búsqueda en una tabla que mapee millones de CIDs a sockets, el programa eBPF simplemente enmascara los bits relevantes para recuperar el entero.
bpf_sk_select_reuseport()realiza el enrutamiento. El ID del worker extraído se usa como índice en el array de sockets, y el kernel entrega el datagrama directamente al socket de ese worker — independientemente de la IP actual del cliente.
Una corrección importante: esta idea de “codificar la info de enrutamiento directamente en el CID” no es solo un truco a medida — es exactamente el problema que el borrador draft-ietf-quic-load-balancers (“QUIC-LB”) del IETF intentó estandarizar, con un diseño de octeto definido (el primer octeto reservado para bits de rotación de configuración/longitud auto-codificada, con el ID del servidor/worker comenzando en el segundo octeto, seguido de un nonce encriptado u obfuscado). Es importante ser precisos sobre su estado: draft-ietf-quic-load-balancers nunca avanzó a RFC y actualmente está listado como expirado/inactivo en el rastreador del IETF. No se convirtió en RFC. Sin embargo, la técnica no es ficticia — muchos balanceadores y proxies reales implementan su propia variante de la misma idea — solo que no es un estándar oficial, sino una convención bien documentada y no oficial.
eBPF no es un entorno de scripting de propósito general
Es importante ser concreto sobre por qué el programa eBPF debe ser tan limitado y barato, en lugar de hacer afirmaciones vagas sobre “restricciones”. El verificador en kernel prueba de forma estática que un programa terminará y será seguro en memoria antes de que se cargue:
- Cada programa tiene un límite de 512 bytes de pila.
- Los bucles sin límite fueron rechazados por completo hasta que Linux 5.3 introdujo “bucles limitados” que terminan de forma comprobada; antes de eso, los bucles debían ser desdoblados en tiempo de compilación.
- El verificador impone un presupuesto de complejidad general (del orden de un millón de instrucciones simuladas por programa), y lo supera rápidamente si hay bucles sin límite o ramificación excesiva en un programa en ruta crítica.
Nada de esto es exótico para una tarea de análisis de encabezados como la extracción del CID, pero ayuda a entender por qué el esquema de codificación del CID es deliberadamente simple (unos pocos bytes, enmascarados directamente) en lugar de algo que requiera una estructura de datos compleja.
Manejo de reinicios: lo que realmente se implementa en producción
El planteamiento original de este problema como “generaciones de socket, similar al enfoque de Cloudflare” subestimó lo concreto que ya es en producción. Cloudflare implementó exactamente esto como un proyecto de código abierto llamado udpgrm (UDP Graceful Restart Marshal), descrito en una publicación de blog técnico de mayo de 2025, y vale la pena revisarlo porque resuelve el problema de actualización de forma más rigurosa que un contador de generaciones hecho a mano.
El problema principal: cuando reinicias o recargas un proxy que termina conexiones QUIC, obtienes dos conjuntos de sockets SO_REUSEPORT en el mismo grupo — uno del binario antiguo, drenando sus conexiones existentes, y otro del nuevo binario, aceptando nuevas. Un enrutador eBPF basado en CID ingenuo simplemente extraería “Worker 2” y entregaría el paquete a nuevo Worker 2, rompiendo todas las conexiones en curso que pertenecían al viejo Worker 2.
Modelo de udpgrm:
- Una generación de socket es el conjunto de sockets del grupo reuseport que pertenecen a una instancia lógica del servidor (es decir, una implementación).
- Un puntero a generación activa indica qué generación debe recibir los flujos nuevos.
- Un dissector de flujo decide, por paquete, si pertenece a un flujo nuevo (para QUIC, un paquete inicial) o a uno establecido, y si establecido, qué generación de socket lo posee originalmente — incluso si es una generación antigua en drenaje.
- El estado del flujo y las referencias a sockets viven en un mapa
SOCKHASHque el daemon llena y mantiene sincronizado desde espacio usuario, desacoplando esa gestión del resto de la aplicación.
udpgrm incluye tres modos de dissector integrados más una plantilla “a medida”: un dissector FLOW que rastrea una tabla hash de 4-tuple de tamaño fijo (útil para protocolos sin identificador de conexión nativo), un dissector CBPF basado en cookies donde el identificador de enrutamiento está incrustado directamente en el paquete — exactamente el esquema de CID de QUIC descrito arriba, que Cloudflare llama una “cookie udpgrm” — y un modo NOOP para protocolos sin estado como DNS que no necesitan nada de esto. El daemon se integra con systemd mediante un pequeño protocolo de control basado en setsockopt/getsockopt y un truco de proceso “decoy” para sortear la suposición de systemd de que solo una instancia de un servicio se ejecuta a la vez.
La conclusión práctica para quien construya esto por sí mismo: no reinventes el seguimiento de generaciones y la dissectión de flujo desde cero a menos que tengas una razón muy específica — udpgrm (o un daemon eBPF similar probado en producción) ya resuelve la mitad más difícil de este problema, que es la de reinicios sin interrupciones.
Cómo queda esto en el ingreso HTTP/3 empresarial
El cambio de TCP a QUIC resuelve un problema real y de larga data en la capa de transporte — pero expone una suposición incrustada en cómo Linux balancea UDP: que un “flujo” está definido por su 4-tuple. QUIC rechaza explícitamente esa suposición, y el comportamiento predeterminado SO_REUSEPORT del kernel no ha actualizado esa lógica por sí solo. BPF_PROG_TYPE_SK_REUSEPORT y bpf_sk_select_reuseport() son los mecanismos actuales para cerrar esa brecha; QUIC-LB es el intento de estandarización (ahora expirado) para la convención de codificación CID; y udpgrm es un ejemplo concreto y de código abierto de cómo sería una versión de producción completa — enrutamiento consciente de migración y reinicios sin interrupciones — en la actualidad.
Fuentes
- RFC 9000 — QUIC: Un transporte multiplexado y seguro basado en UDP (IETF)
- draft-ietf-quic-load-balancers — QUIC-LB: Generación de Connection IDs enrutables (borrador expirado)
- Blog de Cloudflare — “Reinicios de QUIC, problemas de lentitud: udpgrm al rescate”, Marek Majkowski, 7 de mayo de 2025
- Repositorio GitHub de udpgrm
- Documentación eBPF — Tipo de programa
BPF_PROG_TYPE_SK_REUSEPORT - Documentación eBPF — Función auxiliar
bpf_sk_select_reuseport - Documentación eBPF — Bucles
- Commit del kernel Linux — “bpf: Introduce BPF_PROG_TYPE_SK_REUSEPORT”, Martin KaFai Lau
- Vincent Bernat — “Usando eBPF para balancear tráfico entre sockets UDP con Go”
Cambios
Metadatos eliminados: - Se eliminaron los títulos y frases de gancho estilo SEO y el texto final no verificado que parecía metadatos CMS sobrantes en lugar de contenido fuente.
Correcciones:
- Se aclaró que el 0-RTT de QUIC aplica a la reanudación de sesión con clave precompartida, no a cada apretón de manos — una conexión inicial aún requiere un apretón de manos TLS 1.3 de 1-RTT completo.
- Se corrigió el ejemplo de codificación del worker ID en CID: el borrador original decía que el worker ID está en “los primeros dos bytes” del CID. La convención real (IETF QUIC-LB) reserva el primer octeto para bits de rotación de configuración/longitud, y comienza el ID del servidor/worker en el segundo octeto.
- Se añadió el estado de estandarización correcto: draft-ietf-quic-load-balancers nunca pasó a RFC y actualmente está listado como expirado en el rastreador del IETF. Es una convención conocida, no un estándar adoptado.
- Se reemplazó la referencia vaga a “similar al marco udpgrm de Cloudflare” por una descripción detallada y verificada de las mecánicas reales de udpgrm (generación de trabajo, dissectores de flujo, estado SOCKHASH, integración con systemd), extraída directamente del blog técnico de Cloudflare y el README público del proyecto.
- Se confirmó y mantuvo: BPF_PROG_TYPE_SK_REUSEPORT, bpf_sk_select_reuseport(), BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, el límite de 20 bytes del CID de QUIC, y el mecanismo general de rotura por hash del 4-tuple bajo migración — todo verificado contra RFC 9000, la documentación de eBPF, y el commit original del kernel 2018.
Extensiones: - Se añadió detalle concreto sobre las restricciones del verificador eBPF (límite de pila de 512 bytes, rechazo de bucles sin límite antes de Linux 5.3, presupuesto de complejidad) para explicar por qué el programa de steering debe mantenerse mínimo, en lugar de afirmarlo sin soporte. - Se añadió una sección completa sobre los modos de dissectores de udpgrm (FLOW, CBPF, NOOP, BESPOKE) y su integración con systemd, ya que esta es la implementación real del concepto de “generaciones de socket” que solo se mencionaba en el borrador original. - Se añadió una sección de Fuentes con enlaces directos a todas las fuentes principales utilizadas (RFC, borrador IETF, blog de Cloudflare, docs de eBPF, commit del kernel).
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.