Tecnología JIT WireGuard
(fly.io)- Fly.io cambió a un enfoque en el que los peers se agregan al kernel en el momento de la conexión, sin instalarlos de antemano, para reducir la carga de estado de las gateways WireGuard mientras mantiene la comunicación directa entre
flyctly Fly Machines - El flujo anterior consistía en que la API GraphQL enviaba la configuración del peer mediante RPC de NATS,
wggwdla registraba en SQLite y en WireGuard del kernel de Linux, y luegoflyctlse conectaba - La combinación de pérdida de mensajes de NATS y la creación de peers desechables en trabajos de CI hizo que se acumularan cientos de miles de peers no reutilizados en las gateways, ralentizando las operaciones del kernel y la carga durante reinicios
- El nuevo enfoque captura paquetes
handshake initiationmediante un filtro BPF o la ruta de recepción de WebSockets, descifra parte del handshake de Noise para identificar la clave pública y luego obtiene solo los peers necesarios mediante una API HTTP interna - Tras varias semanas en operación, la cantidad de peers antiguos prácticamente desapareció, y las gateways pueden manejar la configuración de peers y los reinicios con menos estado y mayor rapidez
Cómo Fly.io usa WireGuard
- Fly.io ejecuta contenedores como VM basadas en Firecracker y usa WireGuard en varios lugares como parte de las API para clientes
- Al ejecutarse,
flyctlcrea una pila TCP/IP con su propia dirección IPv6 y se comunica directamente con las Fly Machines de la red de Fly.io - Este enfoque facilita presentar funciones como builders remotos de Docker como si estuvieran en la misma LAN, pero es más difícil mantenerlo funcionando de forma confiable
- Fly.io terminó cambiando la ruta predeterminada a WireGuard-over-WebSockets
Flujo anterior de aprovisionamiento de gateways
- Fly.io conecta las conexiones WireGuard que llegan a varias gateways en todo el mundo con la red privada correspondiente
- Cuando
flyctlnecesita comunicarse con una Fly Machine para compilar contenedores, abrir una consola SSH, copiar archivos o hacer proxy de servicios, ejecuta o se conecta a un proceso agente en segundo plano - Cuando el agente se ejecuta por primera vez, crea una nueva configuración de peer WireGuard desde la API GraphQL
- La configuración del peer se compone de una clave pública y la dirección a conectar
- La API envía esa configuración a la gateway correspondiente mediante RPC del sistema de mensajería NATS
wggwden la gateway recibe la configuración, la guarda en SQLite, la agrega al kernel mediante la biblioteca WireGuard para Go y responde a la API que la instalación terminó- Cuando la API devuelve la configuración a la solicitud GraphQL,
flyctlse conecta como un peer WireGuard que ya está instalado en la gateway
Por qué la estructura anterior se volvió lenta
- NATS es rápido, pero no garantiza la entrega, por lo que era difícil usarlo como base de una API confiable
- Fly.io redujo internamente el uso de NATS; por ejemplo, la API interna
flydpasó de estar basada en NATS a estar basada en HTTP - Reducir el uso de NATS mejoró las gateways WireGuard, pero no fue suficiente
- Fly.io redujo internamente el uso de NATS; por ejemplo, la API interna
- Los peers WireGuard creados después de cerrar
flyctlpermanecían en la gateway, y no había un proceso para limpiar peers antiguos- Se había optado por no eliminar los peers porque existía la posibilidad de volver a desplegar al día siguiente o depurar con
fly ssh console - Pero la mayoría de los peers se creaban en trabajos de CI sin almacenamiento persistente, y en la siguiente ejecución no podían reconectarse con el mismo peer, por lo que se creaba uno nuevo cada vez
- Se había optado por no eliminar los peers porque existía la posibilidad de volver a desplegar al día siguiente o depurar con
- Como resultado, las gateways llegaron a tener cientos de miles de peers que quizá nunca se reutilizarían
- A medida que aumentaba el número de peers antiguos, las operaciones de WireGuard en el kernel se volvían muy lentas
- El proceso de volver a cargar todos los peers en el kernel después de reiniciar un servidor gateway era especialmente lento
- También ocurrieron algunos kernel panics
Diseño para instalar peers en el kernel solo cuando se necesitan
- No es difícil guardar todo el historial de peers WireGuard en un único SQLite, pero mantener todos los peers en el kernel de Linux se convierte en un cuello de botella
- Fly.io optó por que la gateway obtenga de la API los peers necesarios bajo demanda, en lugar de empujar la configuración hacia la gateway
- Si se agregan peers al kernel solo cuando un cliente intenta conectarse, los peers antiguos pueden eliminarse del kernel en cualquier momento
- Incluso los peers eliminados pueden volver a obtenerse e instalarse en la siguiente conexión, por lo que se reduce la necesidad de que la gateway mantenga estado de largo plazo
- Sin embargo, WireGuard del kernel de Linux no tiene una API para suscribirse a eventos de “incoming connection attempt”
Cómo se implementan los peers WireGuard JIT
- La interfaz de configuración de WireGuard del kernel de Linux es Netlink, y la biblioteca de control de WireGuard para Go usa wgctrl-go
- Fly.io aprovechó que una solicitud de conexión WireGuard es un paquete identificable para crear eventos directamente con un filtro BPF y un packet socket
- En la ruta WireGuard por WebSockets, es más fácil obtener los paquetes WireGuard sin procesar
- Esta ruta intercambia paquetes UDP enmarcados mediante una conexión WebSockets sin autenticación con la interfaz de gateway
- Como Fly.io controla el código de ese daemon, puede engancharse a la función de recepción de paquetes
- WireGuard no tiene conceptos de “cliente” y “servidor”; es un protocolo punto a punto en el que los peers se conectan entre sí cuando envían tráfico
- Quien se conecta primero es el initiator, y la contraparte es el responder
- En Fly.io, normalmente
flyctles el initiator y la gateway es el responder
- El primer paquete UDP es un
handshake initiationsegún el paper de WireGuard, y el tipo de paquete se registra en un byte en texto plano- Fly.io captura conexiones entrantes con el filtro BPF
udp and dst port 51820 and udp[8] = 1
- Fly.io captura conexiones entrantes con el filtro BPF
Identificar peers en el handshake de Noise
- WireGuard está basado en Noise Protocol Framework, y Noise oculta los identificadores durante el handshake mediante identity hiding
- Por eso no se puede leer directamente del paquete un valor como un nombre de usuario para buscar la configuración
- Fly.io ejecuta parte del cifrado de Noise para descifrar la identidad y así identificar solicitudes entrantes
- Este código es delicado, pero tiene alrededor de 200 líneas
- La interfaz Netlink del kernel puede proporcionar la clave privada de la interfaz a procesos con privilegios, lo que permite obtener el secreto necesario
- El código relacionado está publicado en un gist
- Con este proceso, se puede obtener un feed de eventos de claves públicas de usuarios que intentan conectarse a WireGuard en la gateway
Instalación, caché y optimización de reintentos
- La gateway mantiene en SQLite una caché de limitación de velocidad y, cuando descubre un peer nuevo, obtiene e instala la información del peer correspondiente mediante una solicitud a una API HTTP interna
- Esta lógica encajó bien en el pequeño daemon que ya administraba WireGuard en la gateway
- Los peers antiguos ahora pueden eliminarse activamente con una tarea
cron - La consulta a la API para un peer nuevo puede no ser lo bastante rápida como para responder de inmediato al primer mensaje
handshake initiation- WireGuard reintenta rápidamente, por lo que esto no afecta el funcionamiento en sí
- Fly.io usa una función de Netlink de WireGuard en Linux que Jason Donenfeld les comentó para establecer la conexión más rápido
- A partir del mensaje de initiation entrante, obtiene la dirección 4-tuple, incluido el puerto de origen temporal de
flyctl - La gateway instala el peer como si ella fuera el initiator y
flyctlel responder - El kernel de Linux inicia la conexión WireGuard hacia
flyctl, y el protocolo no depende demasiado de los roles de servidor y cliente - La nueva conexión se establece casi a la velocidad a la que puede instalarse
- A partir del mensaje de initiation entrante, obtiene la dirección 4-tuple, incluido el puerto de origen temporal de
Resultados en producción
- Este enfoque lleva varias semanas ejecutándose en producción
- La cantidad de peers WireGuard antiguos, que iba de miles a cientos de miles por gateway, quedó prácticamente en cero
- Se redujo el estado que deben mantener las gateways
- La configuración de peers se volvió más rápida
- Al reiniciar, disminuyó la necesidad de volver a cargar en el kernel peers que no se usan
1 comentarios
Opiniones en Hacker News
No me queda muy claro eso de que WireGuard en el kernel de Linux no tenga la capacidad de instalar peers cuando se necesitan. Parece que también se pueden agregar peers en tiempo de ejecución: https://serverfault.com/questions/1101002/wireguard-client-a...
Si entendí bien, para entonces ese paso ya es tarde, y lo que intentan es autenticar antes de agregar el peer para que no queden entradas viejas en la interfaz.
Por eso parece una arquitectura en la que ponen un filtro eBPF delante de la interfaz, verifican directamente si la contraparte está autorizada con base en el enrutamiento por clave criptográfica, y si pasa, agregan el peer a la interfaz y lo eliminan después de un timeout.
Los peers podrían estar todos en algo como SQLite y, cuando un cliente intente conectarse, instalarse bajo demanda.
Desde el punto de vista de un proveedor de VPN, la API actual es algo tosca. Aunque en la práctica solo una parte de los peers está en uso en un momento dado, si la cantidad de peers crece de cientos de miles a decenas de millones, se vuelve imposible guardarlos todos en una sola instancia del kernel.
Si hay que instalar los peers por adelantado, al final quedan atados a una máquina de servidor específica.
Como dice el artículo, incluso hoy se puede crear algo parecido a la interfaz necesaria con una simple captura de paquetes, y gracias a que Jason diseñó bien la API, es muy fácil invertir la dirección de inicio entre servidor y cliente. Aunque el kernel haya descartado el primer mensaje de inicio, para el usuario la conexión se siente fluida.
Jann Horn dijo que se podía ir un paso más allá: guardar el paquete de inicio capturado y reinyectarlo al kernel después de instalar el peer; eso también es una idea bastante buena.
No creo que este artículo sea algo que cambie la vida, sino más bien unas cuantas técnicas limpias que a la gente le conviene conocer.
El siguiente paso es construir floating peers sobre esta base para desacoplar por completo los peers de una región. Así el usuario no tendría que preocuparse por en qué región está configurado el peer, y eso parece tener beneficios reales de producto, más allá de ser solo una curiosidad para entusiastas.
La expresión JIT WireGuard se siente un poco rara. Lo primero que pensé fue: “¿por qué? El cuello de botella de rendimiento es el cifrado, y un JIT por cliente no ayudaría en eso”.
Yo simplemente me habría ido a espacio de usuario. Se podría sacar buen rendimiento usando cosas como tokio-uring o glommio.
Si siguen empujando todo dentro del kernel, van a chocar una y otra vez con límites, porque Linux no fue hecho para manejar millones de túneles activos. Incluso tener millones de conexiones TCP en un solo kernel a veces es complicado.
Cada límite requiere un hack, y cada hack trae configuraciones del sistema que hay que aplicar y administrar. La cadena de herramientas para aprovisionar servidores físicos Linux está muy por detrás de las herramientas de desarrollo de apps y servicios y de gestión de configuración.
¿O tal vez soy tonto y entendí algo mal?
Si quieres crear peers de WireGuard en espacio de usuario desde una app en Go, vale la pena mirar este proyecto experimental reciente: https://github.com/dpeckett/noisysockets
Se basa en el excelente trabajo de wireguard-go, pero intenté hacerlo más simple de usar como biblioteca y más idiomático en Go.
Creo que sería interesante construir una malla de servicios con esto. Sería difícil soportar varios lenguajes, pero tal vez se podría implementar una API de sockets.
Eso sí, todavía no he visto aceleración por hardware para el cifrado de WireGuard, así que en rendimiento podría costarle competir con mTLS.
Por cierto, ahora estoy buscando trabajo freelance; si necesitan un freelancer de Golang en redes de alta velocidad y seguridad, pueden contactarme.
Sería algo como Magic Wormhole para túneles arbitrarios, y espero que también pueda mejorar mucho el problema de que las transferencias de archivos en redes largas de alto ancho de banda se caigan a 20–30 MB/s.
0 - https://github.com/slackhq/nebula
En general estoy de acuerdo en que, para mensajes punto a punto, una solicitud HTTP directa puede ser más confiable que pasar por una cola de mensajes, pero me sorprende un poco que se perdieran tantos mensajes en NATS como para afectar mucho al servicio.
Si se pierde un mensaje, ¿NATS no lo retransmite hasta que tenga éxito? Me pregunto si alguien sabe por qué experimentaron una inestabilidad tan perceptible.
La arquitectura de NATS es intuitiva y atractiva, así que me intriga saber dónde se desvió. JetStream tiene muchos parámetros ajustables.
Por ejemplo, se puede configurar un stream en memoria con ventana de detección de duplicados basada en tiempo, modos push/pull, y políticas de retransmisión y confirmación.
Dicho eso, puede que no encaje bien con conexiones de un solo mensaje desechables. En cualquier caso, sería muy útil tener detalles más concretos.
Pero al final no lo necesitábamos. La capa de mensajería, en vez de aportar expresividad, solo hacía más difíciles las pruebas y el monitoreo.
La parte de “configuramos el peer como si nosotros fuéramos el initiator y dejamos a flyctl como responder. El kernel de Linux reinicia la conexión WireGuard hacia flyctl” ¿en la práctica agrega media ida y vuelta de latencia al handshake?
Por ejemplo, me pregunto si el flujo sería algo como 1) flyctl envía Initiation, 2) se agrega el peer vía netlink y se envía un nuevo Initiation, 3) flyctl envía Response
Es decir, quizá no exista el paso 3 o no haga falta esperarlo, y si se impide la nueva iniciación del paso 2, creo que definitivamente no sería así
1.a) Bob no contesta la llamada, pero agrega el número del identificador de llamadas a su libreta de direcciones
No entiendo qué significa eso de que “cada vez que ejecutas flyctl, nuestra enorme y adorable CLI crea un stack TCP/IP de la nada, tiene su propia dirección IPv6 y se comunica directamente con Fly Machines que se ejecutan en nuestra red”
La razón por la que dicen que “crea un stack TCP/IP de la nada” es que normalmente el sistema operativo proporciona el stack TCP/IP como parte del kernel
En wireguard-go, el stack TCP/IP se ejecuta en espacio de usuario, así que puede crearse dentro de un proceso normal de espacio de usuario, como la interfaz de línea de comandos flyctl
Para quienes llevan mucho tiempo trabajando con sistemas, puede parecer bastante mágico. Los stacks TCP/IP en espacio de usuario, dentro de un proceso y realmente utilizables, son algo relativamente nuevo y novedoso
Me pregunto qué impide reinyectar en el stack de red el primer paquete de handshake. Así parecería no haber pérdida de paquetes
También me da curiosidad cuál es el propósito de comprobar
udp[8] = 1en el filtro eBPFComo se dijo en un comentario vecino, el filtro BPF solo captura paquetes de iniciación, y ese es el comportamiento deseado. Es la versión WireGuard de esnifar un SYN para ver el inicio de una conexión TCP
udp[8] = 1filtra solo los paquetes de handshake. Sin eso, los paquetes de datos también se enviarían al daemon en espacio de usuarioNo estoy seguro de si se puede reproducir el primer handshake, pero como WireGuard ignora a clientes desconocidos, quizá sea posible
Es interesante que, por defecto, tunelicen WireGuard sobre WebSocket. No es bueno para el rendimiento, pero para tareas tipo DevOps donde se usa flyctl parece estar bien
También me preguntaba sobre esto al pensar en el futuro de QUIC/HTTP3. No es imposible que los operadores de red, en vez de manejar correctamente UDP en el puerto 443, simplemente lo bloqueen
flyctltiene una opción de configuración para esoSi UDP no funciona, no funciona en absoluto y además es difícil de depurar, así que dejamos como valor predeterminado lo que sabemos con certeza que funciona
Me amarga haber perdido el debate sobre qué valor predeterminado elegir
Mi startup usó Fly durante casi un año. La funcionalidad clave de convertir código en código desplegado en menos de un minuto es realmente hermosa
Levantar y bajar nodos nuevos para backfills también toma solo unos segundos
Pero la empresa en sí me pareció un poco inmadura. Una vez el servidor API estuvo inaccesible en Fly durante 48 horas, y no me quedó claro si fue un error de mi configuración u otra caída “silenciosa”
Tienen un producto de “db”, pero es algo como “no es Postgres administrado”, y ahí también sufrimos cortes constantes
Me parecía raro que agregaran Postgres como un sustantivo de primer nivel en la CLI y luego limitaran el alcance de las funciones soportadas
El acceso a la API del servicio principal también se caía con frecuencia, así que teníamos que esperar para desplegar modificaciones a servicios nuevos
Extraño la experiencia de despliegue, pero, siendo sincero, ahora estoy más satisfecho con Cloud Run de GCP. Tiene muchas menos “sorpresas” y la documentación está mucho más completa
Me sorprende que los proveedores de VPS casi no ayuden a reducir la latencia de servicios backend para los usuarios. Ninguno soporta Anycast, y hay muy pocas opciones de GeoDNS
Aunque GeoDNS agrega complejidad adicional
Ojalá los costos de transferencia de datos de Fly.io fueran más baratos. Ahora, para un servicio parecido a ngrok en el que estoy trabajando, estoy en una situación donde tengo que reimplementar torpemente buena parte de las funcionalidades de Fly.io
[0]: https://lastlogin.io
[1]: El código específico de Fly necesario para ejecutar LastLogin de forma distribuida globalmente es más o menos este: https://github.com/lastlogin-io/obligator/blob/37f75cc861f1b...
Cuando funcionaba bien era realmente fluido, pero no ocurría con la frecuencia suficiente
Quiero aprovechar para presentar Netmaker[0]
No tengo relación con ellos; solo soy alguien que lo usa con satisfacción porque necesitaba acceso privado a AWS VPC en varias cuentas. Me gustaría que se adoptara más ampliamente
[0] https://www.netmaker.io/
En mi trabajo anterior configuré y administré wg con Ansible en algunas máquinas Windows y Linux; estaba bien, pero al final se volvió algo desordenado
El sitio es demasiado ambiguo
La parte de “un gateway con cientos de miles de peers, de los cuales algunos no se volverán a usar” fue exactamente lo que se me vino a la mente al leer los primeros párrafos
También es buena la idea de “no hay una llamada de API para suscribirse a eventos de intentos de conexión entrante. Está bien. Crearemos los eventos nosotros mismos. Las solicitudes de conexión de WireGuard son paquetes y se pueden identificar fácilmente, así que se pueden capturar eficientemente con filtros BPF y sockets de paquetes”
Dicen que, al recibir un mensaje de inicio entrante, se obtiene la dirección de 4-tupla de la conexión deseada, incluido el puerto de origen temporal que usa flyctl, y se instala el peer como si nosotros fuéramos el initiator y flyctl el responder; me pregunto si esto funciona incluso detrás de NAT
{wggwd.fly.io, 12345, clientIP, 23456}Para el UDP NAT en la ruta, un nuevo paquete UDP “initiator” o una respuesta a un mensaje de inicio saliente se ven exactamente igual
Porque su criterio de decisión es solo la 4-tupla, y esa 4-tupla es la misma