3 puntos por GN⁺ 2024-07-05 | 1 comentarios | Compartir por WhatsApp
  • En Firezone usan Rust para construir acceso remoto seguro y escalable en teléfonos Android, computadoras MacOS o servidores Linux
  • Usan una biblioteca de conexión llamada connlib para gestionar conexiones de red y túneles WireGuard
  • Después de varias iteraciones, llegaron a un diseño llamado sans-IO que ofrece pruebas rápidas y exhaustivas, personalización profunda y alta confiabilidad

connlib está escrito en Rust y sigue un diseño sans-IO

  • Gracias a la velocidad y seguridad de memoria de Rust, es adecuado para construir servicios de red
  • Usa el runtime tokio, WebSockets con tungstenite, la implementación de WireGuard boringtun y cifrado de tráfico API con rustls, entre otros
  • El diseño sans-IO implementa protocolos como máquinas de estado puras, en lugar de enviar y recibir bytes por sockets en múltiples lugares

El modelo asíncrono de Rust y la discusión sobre el "coloreado de funciones"

  • Las funciones asíncronas solo pueden llamarse desde otras funciones asíncronas
  • Si una función está en lo profundo de una cadena asíncrona, todas las funciones que la llaman también deben volverse asíncronas
  • Esto puede ser un problema para quienes quieren escribir código indiferente a si sus dependencias son asíncronas o no

Introducción a sans-IO

  • La idea central de sans-IO es similar al principio de inversión de dependencias del mundo OOP
  • La política (qué hacer) no debería depender de los detalles de implementación (cómo hacerlo)
  • En lugar de transmitir datos usando la estructura Transmit, se emite un Transmit

Aplicación de la inversión de dependencias

  • En lugar de transmitir datos usando la estructura Transmit, se emite un Transmit
  • El bucle de eventos implementa los efectos secundarios y llama realmente a UdpSocket::send

Máquina de estado

  • El diagrama de máquina de estado para una solicitud de binding STUN tiene dos estados: Sent y Received
  • La máquina de estado se implementa definiendo la estructura StunBinding y sus funciones relacionadas

Bucle de eventos

  • El bucle de eventos impulsa la máquina de estado y procesa datos usando poll_transmit y handle_input

Abstracción del tiempo

  • Los requisitos basados en tiempo se manejan usando las APIs poll_timeout y handle_timeout

Premisa de sans-IO

  • El diseño sans-IO deja en manos de la aplicación la decisión sobre si las dependencias son asíncronas o no
  • El diseño sans-IO es fácil de componer, ofrece una API flexible, facilita las pruebas y encaja bien con las capacidades de Rust

Composición sencilla

  • La API de StunBinding puede aplicarse a la mayoría de los protocolos de red
  • La biblioteca snownet de Firezone combina ICE y WireGuard para ofrecer un túnel IP "mágico" que funciona sin importar la configuración de red

API flexible

  • Escribir directamente el bucle de eventos permite ajustar el código y facilita su mantenimiento

Pruebas rápidas

  • El código sans-IO no tiene efectos secundarios, por lo que es muy fácil de probar
  • En Firezone implementan una máquina de estado de referencia y realizan pruebas comparándola con el estado real de connlib

Casos límite y fallos de IO

  • El diseño sans-IO separa la implementación del protocolo de los efectos secundarios reales de IO, lo que facilita manejar casos límite y errores

Rust + sans-IO: ¿una combinación perfecta?

  • Rust modela explícitamente la propiedad y la mutabilidad, por lo que encaja muy bien con el diseño sans-IO
  • El diseño sans-IO usa libremente &mut para expresar cambios de estado y, a diferencia de Rust async, solo utiliza APIs síncronas

Desventajas

  • Escribir el bucle de eventos manualmente puede introducir bugs sutiles
  • Los flujos de trabajo secuenciales pueden requerir más código
  • El diseño sans-IO todavía no está muy extendido en la comunidad de Rust

Cierre

  • El código sans-IO al principio puede parecer poco familiar, pero una vez que uno se acostumbra resulta muy disfrutable
  • Rust ofrece herramientas excelentes para modelar máquinas de estado
  • El diseño sans-IO fuerza a tratar el manejo de errores como parte del procesamiento de entradas, y eso se siente como la forma correcta de escribir código de networking

Opinión de GN⁺

  • El diseño sans-IO encaja bien con el modelo de propiedad de Rust, por lo que es muy adecuado para implementar protocolos de red
  • Escribir el bucle de eventos directamente mejora la flexibilidad del código y facilita su mantenimiento
  • La facilidad para hacer pruebas ayuda mucho a escribir código confiable
  • Sin embargo, como todavía no está muy extendido en la comunidad de Rust, puede haber escasez de bibliotecas relacionadas
  • Al adoptar una tecnología nueva, conviene considerar la curva de aprendizaje y el soporte de la comunidad

1 comentarios

 
GN⁺ 2024-07-05
Comentarios de Hacker News
  • Antes de la introducción de la sintaxis async/await en Rust, se implementaban máquinas de estado manualmente

    • Gracias a la sintaxis async/await de Rust, la productividad mejoró muchísimo
    • El async de Rust se transforma automáticamente en una máquina de estado y guarda valores en los puntos de I/O
  • Al escribir una biblioteca VT100, se dio cuenta de un problema con el patrón de encapsulación de Rust

    • Obsesionarse con la encapsulación provoca problemas
    • Esto recuerda que una computadora es una máquina que realiza entrada, transformación de datos y salida
  • Comparado con un diseño que transmite datos usando canales

    • El código se vuelve más complejo
    • Hay que implementar manualmente los tipos de mensajes
    • Se debe proporcionar explícitamente el transmisor
    • Si falla la transmisión por red, no se obtiene el resultado
    • Pero también tiene aspectos convenientes
  • En el ecosistema de Haskell existe la idea de separar la lógica de la ejecución

    • No se menciona cómo se encapsuló la llamada a tokio::select!
    • Había interés en implementar funciones encapsuladas con el estilo sans-IO
  • Las funciones async de Rust se compilan como máquinas de estado

    • Se pregunta si ha habido intentos de combinar sans-IO con async
    • El principal problema es la usabilidad y el manejo de Pin
  • Si se expone el estado, una función async podría volverse "pura"

    • Se intentó hacer binding de OpenSSL a Rust async
  • Firezone es una herramienta sorprendente

    • Se encontró un patrón similar en Rust-libp2p
  • Sería bueno que el compilador pudiera convertir automáticamente código async a sans-IO

    • La conversión manual es propensa a errores
  • Después de leer el artículo y los comentarios, parece que se reinventó el estilo de arquitectura hexagonal o de puertos/adaptadores

  • Hay curiosidad por saber si el tráfico real pasa por el gateway, o si solo se usa para el establecimiento de la conexión