
SwiftNIO SSH es una implementación programática de SSH utilizando SwiftNIO
Este proyecto contiene soporte SSH utilizando SwiftNIO.
SwiftNIO SSH es una implementación programática de SSH: es decir, es una colección de APIs que permiten a los programadores implementar extremos que hablan SSH. Críticamente, esto significa que se parece más a libssh2 que a openssh. SwiftNIO SSH no incluye clientes y servidores SSH listos para producción, sino que proporciona los componentes básicos para construir este tipo de cliente y servidor.
Hay varias razones para proporcionar una implementación programática de SSH. Una es que SSH tiene una relación única con la interactividad del usuario. Los usuarios técnicos están muy acostumbrados a interactuar con SSH de forma interactiva, ya sea para ejecutar comandos en máquinas remotas o para ejecutar shells interactivos. Tener la capacidad de responder programáticamente a estas solicitudes permite modos alternativos de interacción interesantes. Como ejemplos anteriores, podemos señalar el Manhole de Twisted, que utiliza una implementación programática de SSH llamada conch para proporcionar un intérprete interactivo de Python dentro de un servidor Python en ejecución, o ssh-chat, un servidor SSH que proporciona una sala de chat en lugar de la funcionalidad normal de shell SSH. También se pueden imaginar usos innovadores para el reenvío TCP.
Otra buena razón para proporcionar SSH programático es que no es raro que los servicios necesiten interactuar con otros servicios de una manera que implique ejecutar comandos. Mientras que Process resuelve esto para el caso local, a veces los comandos que deben invocarse son remotos. Si bien Process podría lanzar un cliente ssh como subproceso para ejecutar esta invocación, puede ser sustancialmente más directo simplemente invocar SSH directamente. Este es el caso de uso objetivo de libssh2. SwiftNIO SSH proporciona el equivalente de la capa de red y criptográfica de libssh2, permitiendo a los usuarios motivados manejar sesiones SSH directamente desde servicios Swift.
Las versiones más recientes de SwiftNIO SSH admiten Swift 5.9 y posteriores. Las versiones mínimas de Swift compatibles con las versiones de SwiftNIO SSH se detallan a continuación:
SwiftNIO SSH soporta SSHv2 con el siguiente conjunto de características:
SwiftNIO SSH proporciona un ChannelHandler de SwiftNIO, NIOSSHHandler. Este manejador implementa la mayor parte del protocolo SSH directamente. No se espera que los usuarios generen mensajes SSH directamente: en su lugar, interactúan con el NIOSSHHandler a través de canales hijos y delegados.
SSH es un protocolo multiplexado: cada conexión SSH se subdivide en múltiples canales de comunicación bidireccionales llamados, adecuadamente, canales. SwiftNIO SSH refleja esta construcción utilizando una abstracción de "canal hijo". Cuando un par crea un nuevo canal SSH, SwiftNIO SSH creará un nuevo Channel de NIO que se utiliza para representar todo el tráfico en ese canal SSH. Dentro de este Channel hijo, todos los eventos están estrictamente ordenados entre sí: sin embargo, los eventos en diferentes Channels pueden intercalarse libremente por la implementación.
Una conexión SSH activa, por lo tanto, se ve así:
┌ ─ NIO Channel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌────────────────────────────────┐ │
│ │
│ │ │ │
│ │
│ │ │ │
│ NIOSSHHandler │───────────────────────┐
│ │ │ │ │
│ │ │
│ │ │ │ │
│ │ │
│ └────────────────────────────────┘ │ │
│
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│
│
│
▼
┌── SSH Child Channel ─────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ├───┐
│ │ │ │ │ │ │
│ │ │ │ │ │ ├───┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ User Handler │ │ User Handler │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────┘ └────────────────────────────────┘ │ │ │
│ │ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
Un canal SSH se invoca con un tipo de canal. NIOSSH soporta tres: session, directTCPIP y forwardedTCPIP. El tipo de canal más común es session: session se utiliza para representar la invocación de un programa, ya sea un programa específico con nombre o un shell. Los otros dos tipos de canal están relacionados con el reenvío de puertos TCP, y se discutirán más adelante.
Un canal SSH opera sobre un único tipo de datos: SSHChannelData. Esta estructura encapsula el hecho de que SSH soporta tanto datos de canal regulares como "extendidos". Los datos de canal regulares (SSHChannelData.DataType.channel) se utilizan para la gran mayoría de los datos centrales. En los canales session, el tipo de datos .channel se usa para la entrada estándar y la salida estándar: el tipo de datos .stdErr se usa para el error estándar (naturalmente). En los canales de reenvío TCP, el tipo de datos .channel es el único utilizado y representa los datos reenviados.
Un canal session representa una invocación de un comando. Exactamente cómo opera el canal se comunica a través de varios eventos de usuario entrantes. Los siguientes eventos son importantes:
SSHChannelRequestEvent.PseudoTerminalRequest: Solicita la asignación de un pseudo-terminal.SSHChannelRequestEvent.EnvironmentRequest: Solicita una única variable de entorno para la invocación del comando. Siempre se envía antes del comando en sí.SSHChannelRequestEvent.ShellRequest: Solicita que el comando a invocar sea el shell del usuario autenticado.SSHChannelRequestEvent.ExecRequest: Solicita la invocación de un comando específico.SSHChannelRequestEvent.ExitStatus: Se utiliza para señalar que el comando remoto ha salido, y comunica el código de salida.SSHChannelRequestEvent.ExitSignal: Se utiliza para indicar que el comando remoto fue terminado en respuesta a una señal, y cuál fue esa señal.SSHChannelRequestEvent.SignalRequest: Se utiliza para enviar una señal al comando remoto.SSHChannelRequestEvent.LocalFlowControlRequest: Se utiliza para indicar si el cliente es capaz de realizar el control de flujo Ctrl-Q/Ctrl-S por sí mismo.SSHChannelRequestEvent.WindowChangeRequest: Se utiliza para comunicar un cambio en el tamaño de la ventana del terminal en el cliente al pseudo-terminal asignado.Estos eventos no se utilizan en los mensajes de reenvío de puertos. Las implementaciones SSH que soportan canales de tipo .session deben estar preparadas para manejar la mayoría o todos estos de varias maneras.
Cada uno de estos eventos también tiene un campo wantReply. Esto indica si la solicitud necesita una respuesta para indicar éxito o fracaso. Si es así, se utilizan los siguientes dos eventos:
ChannelSuccessEvent, para comunicar éxito.ChannelFailureEvent, para comunicar fracaso.El protocolo de red SSH utiliza de manera generalizada el semi-cierre en los canales hijos. Los Channels de NIO típicamente tienen el soporte de semi-cierre deshabilitado por defecto, y SwiftNIO SSH respeta este valor predeterminado en sus canales hijos también. Sin embargo, si deja esta configuración en su valor predeterminado, los canales hijos SSH se comportarán de manera extremadamente inesperada. Por esta razón, se recomienda encarecidamente que todos los canales hijos tengan habilitado el soporte de semi-cierre:
channel.setOption(ChannelOptions.allowRemoteHalfClosure, true)
Esto luego utiliza el soporte de semi-cierre estándar de NIO. El par remoto que envía EOF se comunicará con un evento de usuario entrante, ChannelEvent.inputClosed. Para enviar EOF usted mismo, llame a close(mode: .output).
La autenticación de usuario es una parte vital de SSH. Para gestionarla, SwiftNIO SSH utiliza un par de protocolos delegados: NIOSSHClientUserAuthenticationDelegate y NIOSSHServerUserAuthenticationDelegate. Los clientes y servidores deben proporcionar implementaciones de estos protocolos delegados para gestionar la autenticación de usuario.
El protocolo del cliente es sencillo: SwiftNIO SSH invocará el método nextAuthenticationType(availableMethods:nextChallengePromise:) en el delegado. El availableMethods será una instancia de NIOSSHAvailableUserAuthenticationMethods que comunica qué métodos de autenticación el servidor ha sugerido que serán aceptables. El delegado puede entonces completar nextChallengePromise con una nueva solicitud de autenticación, o con nil para indicar que el cliente se ha quedado sin opciones.
El protocolo del servidor es más complejo. El delegado debe proporcionar una propiedad supportedAuthenticationMethods que comunique qué métodos de autenticación son soportados por el delegado. Luego, cada vez que el cliente envía una solicitud de autenticación de usuario, se invocará el método requestReceived(request:responsePromise:). Esto puede invocarse varias veces en paralelo, ya que los clientes pueden emitir solicitudes de autenticación en paralelo. El responsePromise debe ser completado con éxito con el resultado de la autenticación. Hay tres resultados: .success y .failure son directos, pero en principio el servidor puede requerir múltiples desafíos usando .partialSuccess(remainingMethods:).
El reenvío de puertos directo es el reenvío de puertos del cliente al servidor. En este modo, tradicionalmente el cliente escuchará en un puerto local y reenviará las conexiones entrantes al servidor. Solicitará que el servidor reenvíe estas conexiones como conexiones salientes a un host y puerto específicos.
Estos canales pueden ser abiertos directamente por los clientes utilizando el tipo de canal .directTCPIP.
El reenvío de puertos remoto es una situación menos común donde el cliente solicita al servidor que escuche en una dirección y puerto específicos, y que reenvíe todas las conexiones entrantes al cliente. Como el cliente necesita solicitar este comportamiento, lo hace mediante solicitudes globales.
Las solicitudes globales se inician usando NIOSSHHandler.sendGlobalRequest, y se reciben y manejan mediante un GlobalRequestDelegate. Actualmente se soportan dos solicitudes globales:
GlobalRequest.TCPForwardingRequest.listen(host:port:): una solicitud para que el servidor escuche en un host y puerto determinados.GlobalRequest.TCPForwardingRequest.cancel(host:port:): una solicitud para cancelar la escucha en el host y puerto determinados.Los servidores pueden ser notificados y responder a estas solicitudes usando un GlobalRequestDelegate. El método a implementar aquí es tcpForwardingRequest(_:handler:promise:). Este método delegado se invocará cada vez que se reciba una solicitud global. La respuesta a la solicitud se pasa a promise.
Luego, los canales reenviados se envían del servidor al cliente utilizando el tipo de canal .forwardedTCPIP.
| SwiftNIO SSH | Versión mínima de Swift |
|---|
0.0.0 ..< 0.3.0 | 5.1 |
0.3.0 ..< 0.4.0 | 5.2 |
0.4.0 ..< 0.5.0 | 5.4 |
0.5.0 ..< 0.6.2 | 5.5.2 |
0.6.2 ..< 0.9.0 | 5.6 |
0.9.0 ..< 0.9.2 | 5.8 |
0.9.2 ..< 0.10.0 | 5.9 |
0.10.0 ... 0.12.0 | 5.10 |
0.12.0 ..< 0.13.0 | 6.0 |
0.13.0 ..< | 6.1 |
SSHChannelRequestEvent.SubsystemRequest: Se utiliza para solicitar la invocación de un subsistema específico. El significado de esto es específico para casos de uso individuales.