SwiftNIO SSH

本项目包含使用 SwiftNIO 的 SSH 支持。

什么是 SwiftNIO SSH?

SwiftNIO SSH 是 SSH 的程序化实现:也就是说,它是一系列 API,允许程序员实现支持 SSH 的端点。关键的是,它更像是 libssh2 而不是 openssh。SwiftNIO SSH 不提供可以直接用于生产环境的 SSH 客户端和服务器,而是提供构建此类客户端和服务器的基础组件。

提供程序化的 SSH 实现有许多原因。其中一个原因是 SSH 与用户交互有着独特的关系。技术用户非常习惯于与 SSH 进行交互,要么在远程计算机上运行命令,要么运行交互式 shell。能够以编程方式响应这些请求可以实现有趣的替代交互模式。作为之前的例子,我们可以指出 Twisted 的 Manhole,它使用 一个名为 conch 的程序化 SSH 实现,以在运行中的 Python 服务器中提供交互式 Python 解释器,或者 ssh-chat,一个提供聊天室而不是常规 SSH shell 功能的 SSH 服务器。对于 TCP 转发也可以想象出创新的用途。

提供程序化 SSH 的另一个好原因是,服务常常需要以涉及运行命令的方式与其他服务进行交互。虽然 Process 解决了本地用例的问题,但有时需要调用的命令是远程的。虽然 Process 可以启动一个 ssh 客户端作为子进程来运行此调用,但直接调用 SSH 可能会更加简单直接。这就是 libssh2 的目标用例。SwiftNIO SSH 提供了相当于 libssh2 的网络和加密层,允许有积极性的用户直接从 Swift 服务内部驱动 SSH 会话。

最新版本的 SwiftNIO SSH 支持 Swift 5.9 及更高版本。SwiftNIO SSH 版本支持的最低 Swift 版本如下详述

SwiftNIO SSH 最低 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 ... 5.9

SwiftNIO SSH 支持什么?

SwiftNIO SSH 支持具有以下功能的 SSHv2

如何使用 SwiftNIO SSH?

SwiftNIO SSH 提供了一个 SwiftNIO ChannelHandlerNIOSSHHandler。此处理程序直接实现了大部分 SSH 协议。不期望用户直接生成 SSH 消息:相反,他们通过子通道和委托与 NIOSSHHandler 进行交互。

SSH 是一种多路复用协议:每个 SSH 连接被细分为多个双向通信通道,恰如其分地称为通道。SwiftNIO SSH 通过使用“子通道”抽象来反映这种结构。当对等方创建一个新的 SSH 通道时,SwiftNIO SSH 将创建一个新的 NIO Channel,用于表示该 SSH 通道上的所有流量。在此子 Channel 中,所有事件都彼此严格排序:但是,不同 Channel 中的事件可能会被实现自由地交错。

因此,一个活动的 SSH 连接看起来像这样

┌ ─ NIO Channel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐

│     ┌────────────────────────────────┐    │
      │                                │
│     │                                │    │
      │                                │
│     │                                │    │
      │         NIOSSHHandler          │───────────────────────┐
│     │                                │    │                  │
      │                                │                       │
│     │                                │    │                  │
      │                                │                       │
│     └────────────────────────────────┘    │                  │
                                                               │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘                  │
                                                               │
                                                               │
                                                               │
                                                               │
                                                               ▼
                     ┌── SSH Child Channel ─────────────────────────────────────────────────────────────┐
                     │                                                                                  │
                     │   ┌────────────────────────────────┐      ┌────────────────────────────────┐     ├───┐
                     │   │                                │      │                                │     │   │
                     │   │                                │      │                                │     │   ├───┐
                     │   │                                │      │                                │     │   │   │
                     │   │                                │      │                                │     │   │   │
                     │   │          User Handler          │      │          User Handler          │     │   │   │
                     │   │                                │      │                                │     │   │   │
                     │   │                                │      │                                │     │   │   │
                     │   │                                │      │                                │     │   │   │
                     │   │                                │      │                                │     │   │   │
                     │   └────────────────────────────────┘      └────────────────────────────────┘     │   │   │
                     │                                                                                  │   │   │
                     └───┬──────────────────────────────────────────────────────────────────────────────┘   │   │
                         │                                                                                  │   │
                         └───┬──────────────────────────────────────────────────────────────────────────────┘   │
                             │                                                                                  │
                             └──────────────────────────────────────────────────────────────────────────────────┘

SSH 通道使用通道类型调用。NIOSSH 支持三种:sessiondirectTCPIPforwardedTCPIP。最常见的通道类型是 sessionsession 用于表示程序的调用,无论是特定的命名程序还是 shell。其他两种通道类型与 TCP 端口转发相关,将在后面讨论。

SSH 通道对单个数据类型进行操作:SSHChannelData。此结构封装了 SSH 支持常规和“扩展”通道数据这一事实。常规通道数据 (SSHChannelData.DataType.channel) 用于绝大多数核心数据。在 session 通道中,.channel 数据类型用于标准输入和标准输出:.stdErr 数据类型用于标准错误(自然)。在 TCP 转发通道中,.channel 数据类型是唯一使用的数据类型,表示转发的数据。

通道事件

session 通道表示命令的调用。通道如何运行的确切方式通过许多入站用户事件进行通信。以下事件很重要

这些事件在端口转发消息中未使用。支持 .session 类型通道的 SSH 实现需要准备好以各种方式处理大多数或所有这些事件。

每个事件还有一个 wantReply 字段。这指示请求是否需要回复来指示成功或失败。如果需要,则使用以下两个事件

半关闭

SSH 网络协议在子通道中普遍使用半关闭。默认情况下,NIO Channel 通常禁用半关闭支持,SwiftNIO SSH 在其子通道中也遵循此默认设置。但是,如果将此设置保留为其默认值,则 SSH 子通道的行为将非常出乎意料。因此,强烈建议所有子通道都启用半关闭支持

channel.setOption(ChannelOptions.allowRemoteHalfClosure, true)

然后,这使用标准的 NIO 半关闭支持。远程对等方发送 EOF 将通过入站用户事件 ChannelEvent.inputClosed 进行通信。要自己发送 EOF,请调用 close(mode: .output)

用户身份验证

用户身份验证是 SSH 的重要组成部分。为了管理它,SwiftNIO SSH 使用一对委托协议:NIOSSHClientUserAuthenticationDelegateNIOSSHServerUserAuthenticationDelegate。客户端和服务器应提供这些委托协议的实现来管理用户身份验证。

客户端协议很简单:SwiftNIO SSH 将在委托上调用方法 nextAuthenticationType(availableMethods:nextChallengePromise:)availableMethods 将是 NIOSSHAvailableUserAuthenticationMethods 的实例,它传达了服务器建议的哪些身份验证方法是可以接受的。然后,委托可以使用新的身份验证请求完成 nextChallengePromise,或者使用 nil 指示客户端已经用尽了所有尝试。

服务器协议更复杂。委托必须提供一个 supportedAuthenticationMethods 属性,该属性传达委托支持哪些身份验证方法。然后,每次客户端发送用户身份验证请求时,都会调用 requestReceived(request:responsePromise:) 方法。这可能会并行调用多次,因为允许客户端并行发出身份验证请求。responsePromise 应该使用身份验证结果来获得成功。有三个结果:.success.failure 很简单,但原则上服务器可以使用 .partialSuccess(remainingMethods:) 要求多个质询。

直接端口转发

直接端口转发是从客户端到服务器的端口转发。在这种模式下,传统上,客户端将侦听本地端口,并将入站连接转发到服务器。它将要求服务器将这些连接作为出站连接转发到特定的主机和端口。

客户端可以使用 .directTCPIP 通道类型直接打开这些通道。

远程端口转发和全局请求

远程端口转发是一种不太常见的情况,客户端要求服务器侦听特定的地址和端口,并将所有入站连接转发给客户端。由于客户端需要请求此行为,因此它使用全局请求来执行此操作。

使用 NIOSSHHandler.sendGlobalRequest 启动全局请求,并通过 GlobalRequestDelegate 接收和处理全局请求。目前支持两个全局请求

可以使用 GlobalRequestDelegate 通知服务器并响应这些请求。这里要实现的方法是 tcpForwardingRequest(_:handler:promise:)。只要收到全局请求,就会调用此委托方法。对请求的响应被传递到 promise 中。

然后,使用 .forwardedTCPIP 通道类型将转发的通道从服务器发送到客户端。