SSH, modo de usuario, TCP / IP y WireGuard

Cualquiera que aloje una aplicación de un proveedor como Fly.io (en adelante simplemente Fly) puede necesitar conectarse al servidor que ejecuta esta aplicación a través de SSH.



Pero Fly es como una oveja negra entre otras plataformas similares. Nuestro hardware funciona en centros de datos repartidos por todo el mundo. Nuestros servidores están conectados a Internet a través de la red Anycast y están conectados entre sí mediante la red WireGuard. Tomamos contenedores Docker de los usuarios y los convertimos en microvirtuales Firecracker. Y cuando comenzamos, hicimos precisamente eso para brindarles a nuestros clientes la capacidad de ejecutar "aplicaciones de borde". Estas aplicaciones suelen ser fragmentos de código autónomos relativamente pequeños que son muy sensibles al rendimiento de la red. Como resultado, estos fragmentos de código deben ejecutarse en servidores ubicados lo más cerca posible de los usuarios. En tal entorno, la capacidad de conectarse al servidor a través de SSH no es tan importante.







Pero ahora no todos nuestros clientes utilizan Fly de esta forma. Hoy en día, en el entorno Fly, puede ejecutar fácilmente todo el código relacionado con una aplicación. Hemos simplificado el procedimiento para iniciar un conjunto de servicios en un entorno agrupado. Dichos servicios pueden, utilizando canales de comunicación seguros, interactuar entre sí, pueden almacenar datos de forma permanente, pueden, a través de la red WireGuard, comunicarse con sus operadores. Si continúo la historia sobre nuestro sistema con el mismo espíritu, tendré que proporcionar enlaces a todos los materiales que hemos escrito durante los últimos meses.



Pero, en cualquier caso, no teníamos soporte SSH normal.



Está claro, por supuesto, que simplemente puede crear un contenedor con un servicio SSH al que pueda conectarse a través de SSH. La plataforma Fly admite el trabajo con puertos TCP comunes (y también puertos UDP ). Si el cliente, usando el archivo fly.toml



, "le dice" a nuestra red Anycast sobre su extraño puerto SSH, el sistema organizará el enrutamiento de sus conexiones SSH, luego de lo cual todo funcionará como debería.



Pero quienes crean contenedores generalmente no lo hacen, y no sugerimos que lo hagan. Como resultado, equipamos Fly con soporte SSH. Lo que hemos hecho está organizado de una manera bastante inusual. En este artículo, que consta de dos partes, hablaré de esto.



Parte 1: 6PN y Hallpass



Escribí mucho sobre cómo se organizan las redes privadas en Fly. En pocas palabras, resulta que lo que tenemos se puede comparar con una versión IPv6 simplificada de las "nubes privadas virtuales" GCP o AWS. A este sistema lo llamamos 6PN. Cuando se lanza una instancia de aplicación (máquina microvirtual Firecracker) en Fly, asignamos un prefijo IPv6 especial a esta instancia. Varios identificadores están codificados en el prefijo: el identificador de la aplicación, la organización propietaria de la aplicación y los recursos de hardware en los que se ejecuta la aplicación. Usamos un poco de código eBPF para enrutar estáticamente dichos paquetes IPv6 en nuestra red WireGuard interna y para asegurarnos de que los clientes no puedan conectarse a los sistemas de organizaciones con las que no están involucrados.



También puede usar WireGuard para conectar las redes IPv6 privadas que creamos con otras redes. Nuestra API puede crear configuraciones WireGuard que se pueden utilizar, por ejemplo, en hosts EC2 para el proxy RDS Postgres . O, si es necesario, puede utilizar clientes WireGuard (en Windows, Linux o macOS) para conectar la computadora de desarrollo a su propia red privada.



Probablemente ya sepas a qué me refiero.



Escribimos un servidor SSH muy pequeño y muy simple en Go llamado Hallpass. Se puede comparar con "¡Hola, mundo!" Creado con la biblioteca Go. x/crypto/ssh



... (Si hiciera esto de nuevo, probablemente usaría el paquete Glider Labs para construir servidores SSH. Usando este paquete, nuestro servidor sería literalmente un "¡Hola, mundo!". Se realiza la inicialización de todas las instancias de máquinas microvirtuales Firecracker y Hallpass es lanzado con enlace a sus direcciones 6PN.



Si puede operar en la red 6PN de su organización (por ejemplo, a través de una conexión WireGuard), eso significa que puede iniciar sesión en la instancia microvirtual usando Hallpass.



Solo hay un detalle interesante sobre cómo funciona Hallpass. Se trata de autenticación. Los elementos de infraestructura de nuestra red de producción no suelen tener acceso directo a nuestras API ni a sus bases de datos subyacentes. Y las propias instancias de Firecracker, por supuesto, tampoco tienen este acceso. Esto conduce a algunas dificultades asociadas con el cambio de la configuración de comunicación. ¿Cómo, por ejemplo, puede responder a la pregunta de qué tipo de claves necesita tener para conectarse a ciertas instancias de máquinas microvirtuales?



Encontramos una solución para este problema recurriendo a certificados de cliente SSH. En lugar de tener que lidiar con la entrega de las claves cada vez que un usuario quiere iniciar sesión desde un nuevo host, creamos un certificado raíz para organizar a ese usuario. La clave pública de este certificado raíz está alojada en nuestro sistema DNS privado, y Hallpass se pone en contacto con el DNS para obtener este certificado cada vez que se intenta iniciar sesión. Nuestra API firma nuevos certificados para los usuarios, estos certificados se pueden utilizar para iniciar sesión en el sistema.



Es posible que tenga preguntas sobre esta solución. Por lo tanto, revelaré algunos detalles más sobre él.



Primero, hablemos de certificados. Décadas de locura X.509”Puede haber causado que la palabra“ certificado ”le dé un regusto desagradable. Y no te culpo por eso. Pero los certificados deben usarse al organizar conexiones SSH, ya que dichos certificados en este caso son una buena solución. Sin embargo, los certificados SSH no son certificados X.509. Utiliza su propio formato OpenSSH y, en general, no se puede decir nada especial sobre estos certificados. Ellos, como todos los demás certificados, tienen una "fecha de vencimiento", que le permite crear claves de corta duración (y esto es, casi siempre, exactamente lo que necesita). Y, por supuesto, le permiten asignar una clave pública a un grupo completo de servidores, lo que puede autorizar una cantidad arbitraria de claves privadas. No es necesario actualizar constantemente los servidores correspondientes.



Lo siguiente es nuestra API y la firma de certificados. ¡Bien! Somos muy cuidadosos, pero estos certificados son generalmente tan seguros como los tokens de acceso Fly. Por el momento, los certificados no pueden estar mejor protegidos que los tokens, ya que el token permite el despliegue de nuevas versiones de contenedores de aplicaciones. Trabajar con Web PKI X.509 CA implica muchas formalidades. Prescindimos de ellos.



Y finalmente, nuestro DNS. Ella, estoy de acuerdo, parece una completa tontería. Pero realmente no es tan malo. Cada host que ejecuta instancias microvirtuales de Firecracker ejecuta una versión local de nuestro servidor DNS privado (un pequeño programa escrito en Rust). El código eBPF asegura que las máquinas Firecracker solo puedan interactuar con este servidor DNS, refiriéndose a él desde la dirección 6PN de su servidor. (Desde un punto de vista técnico, un usuario solo puede realizar consultas a la API de DNS privada de este servidor, y todas las consultas de los demás usuarios se procesarán de forma recursiva). Un servidor DNS puede (sé que parece inusual) identificar de manera confiable una organización analizando las solicitudes de direcciones IP de origen. En general, así es como trabajamos.



Todo esto sucede en las profundidades de nuestro sistema, los usuarios no pueden ver todo esto. Los usuarios vieron solo un comando flyctl ssh issue -a



que solicitó un nuevo certificado de nuestra API y luego lo pasaron al agente SSH local, después de lo cual las conexiones SSH, en general, resultaron estar operativas. Todo esto se organizó con bastante pulcritud. Pero cualquier negocio siempre se puede hacer con más precisión que antes.



Parte 2: trabajar en una red WireGuard desde el modo de usuario usando TCP / IP



Hay un problema con el esquema anterior de usar SSH, que es que no todos tienen WireGuard instalado. Sin embargo, todos deben instalar el programa correspondiente. WireGuard es una gran tecnología que ayuda mucho a administrar las aplicaciones que se ejecutan en la plataforma Fly. Pero, sea como sea, algunos de nuestros usuarios no tienen WireGuard.



Es cierto que estos usuarios también necesitan trabajar con sus sistemas a través de SSH.



A primera vista, el hecho de que alguien no tenga WireGuard instalado puede parecer un obstáculo insuperable. ¿Cómo funciona WireGuard? Se crea una nueva interfaz de red en la computadora del usuario. Esta es una interfaz WireGuard a nivel de kernel (en Linux) o un túnel con un servicio WireGuard en modo de usuario adjunto (en todos los demás sistemas operativos). Sin esta interfaz de red, no puede trabajar con la red WireGuard.



Pero si mira WireGuard desde el ángulo correcto, puede ver que, desde un punto de vista técnico, este no es el caso. Es decir, se requieren privilegios de nivel de sistema operativo para configurar una nueva interfaz de red. Pero para enviar paquetes a 51820/udp



no se necesitan privilegios. Todo lo necesario para que funcione el protocolo WireGuard se puede iniciar como un proceso sin privilegios que se ejecuta en modo de usuario. Así es como funciona el paquete wireguard-go .



Esto solo le permitirá pasar por el procedimiento de protocolo de enlace de WireGuard. Pero al mismo tiempo, no estamos hablando del intercambio de información con los nodos de la red WireGuard, ya que no se puede simplemente tomar y enviar algunos datos arbitrarios a otro sistema conectado a esta red. Un sistema de este tipo escucha los paquetes que normalmente se transmitirían a través de redes TCP / IP. Las herramientas estándar del sistema que admiten sockets UDP no ayudan a establecer una conexión TCP utilizando dichos sockets.



¿Sería difícil escribir un pequeño fragmento de código que habilite TCP en modo de usuario, diseñado únicamente para admitir la comunicación a través de la red WireGuard, nuevamente en modo de usuario? Dicho código permitiría a los usuarios de Fly conectarse a sus sistemas a través de SSH sin tener que instalar el software que alimenta WireGuard.



Fui imprudente al discutir todo esto en el canal de Slack en el que estaba Jason Donenfeld. Es decir, después de pensar en voz alta, me fui a la cama. Cuando me desperté, Jason ya había implementado todo esto usando gVisor y lo había incluido en la biblioteca WireGuard.



Lo más interesante aquí es gVisor. Ya escribimos sobre eso ... Si alguien no lo sabe, gVisor es esencialmente un sistema operativo Linux de espacio de usuario, Linux implementado en Golang, que se usa como reemplazo runc



de los contenedores en ejecución. Este es en realidad un proyecto completamente loco. Y si lo usas, supongo que puedes contárselo a los demás con orgullo, porque es algo maravilloso. En su profundidad, existe una implementación TCP / IP completa, escrita en Go, que opera sobre datos de entrada y salida representados como búferes ordinarios []byte



.



Luego, se tuitearon algunos tweets y, un par de horas después, recibí un correo electrónico muy agradable de Ben Barkert.... Ben ya había trabajado en varias tareas relacionadas con el subsistema de redes gVisor, estaba interesado en lo que estábamos trabajando, quería saber si nos gustaría cooperar con él. Nos gustó su idea de trabajar juntos en este proyecto. Y ahora, sin entrar en detalles, tenemos una implementación de SSH basada en certificados que se ejecuta a través de la implementación de gVisor TCP / IP en modo de usuario. Todo esto interactúa con la red WireGuard a través de un paquete de modo personalizado wireguard-go



. Y finalmente, esta cosa está integrada en flyctl



.



Para usar SSH flyctl



, simplemente ingrese un comando como este:



flyctl ssh shell personal dogmatic-potato-342.internal

      
      





Y ahora, para que te des cuenta de lo increíble de lo que está pasando, te contaré un poco sobre este comando. Entonces, dogmatic-potato-342.internal



es un nombre DNS interno que solo se resuelve mediante un servidor DNS privado en la red 6PN. Todo esto es eficiente debido al hecho de que en el modo la ssh shell



utilidad flyctl



usa el modo de usuario gVisor de la pila TCP / IP. Pero no hay ningún código en gVisor para realizar una búsqueda de DNS. Esta es solo una biblioteca Go estándar que engañamos al deslizar nuestra interfaz TCP / IP especial en ella.



Flyctl



, por cierto, este es un proyecto de código abierto(Debería ser así, ya que los clientes necesitan usarlo en sus propias computadoras en las que están involucrados en el desarrollo). Por lo tanto, si está interesado, puede leer su código. Ben escribió un buen código en la carpeta pkg . Y el resto del código, horrible, escribí. En Go, proporcionar comunicaciones IP en la red WireGuard es sorprendentemente simple. Si alguna vez ha realizado programación TCP / IP de bajo nivel, entonces puede encontrar esta simplicidad increíble. Los objetos de la pila TCP de gVisor se conectan directamente al código de red de la biblioteca estándar.



Eche un vistazo a este código:



tunDev, gNet, err := netstack.CreateNetTUN(localIPs, []net.IP{dnsIP}, mtu)
if err != nil {
    return nil, err
}

// ...

wgDev := device.NewDevice(tunDev, device.NewLogger(cfg.LogLevel, "(fly-ssh) "))

      
      





CreateNetTUN



Es parte wireguard-go



. Aquí es donde se utilizan las capacidades de gVisor. En primer lugar, tenemos a nuestra disposición un dispositivo de túnel sintético que se puede utilizar para leer y escribir paquetes ordinarios que proporcionan la operación WireGuard. En segundo lugar, tenemos la función net.Dialer , un contenedor para gVisor, que se puede utilizar en el código Go y a través de él interactuar con la red WireGuard correspondiente.



Es todo? En general, sí. Por ejemplo, así es como usamos estos mecanismos para trabajar con DNS:



resolv: &net.Resolver{
    PreferGo: true,
    Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
        return gNet.DialContext(ctx, network, net.JoinHostPort(dnsIP.String(), "53"))
    },
},

      
      





Este es un código de red normal escrito en Go. En general, salió bien.



Obviamente, todos deberían hacer esto.



Gracias a un par de cientos de líneas de código (esto es, aparte del código de implementación del modo de usuario de Linux que obtenemos de gVisor; pero qué hacer, no hay escape de las dependencias), puede obtener una nueva red con criptográfico autenticación a su disposición. Una red accesible en cualquier momento y desde casi cualquier programa.



Está claro que una red de este tipo es significativamente más lenta que la basada en la implementación principal de TCP / IP. Pero, ¿es a menudo realmente importante? Y, en particular, ¿tiene a menudo algún significado a la hora de resolver problemas que surgen periódicamente, para cuya solución se suelen construir túneles TLS extraños, desconocidos de qué? Cuando la velocidad importa, simplemente puede cambiar a la implementación normal de WireGuard.



En cualquier caso, lo que dije resolvió nuestro gran problema. Después de todo, este sistema es adecuado no solo para organizar el trabajo de SSH. También alojamos bases de datos de Postgres. Es muy conveniente cuando es posible, mediante la ejecución de un comando simple, abrir literalmente un shell desde cualquier lugar psql



, independientemente de si es posible, en el momento adecuado, instalar WireGuard para macOS.



¿Está utilizando WireGuard?






All Articles