# SwiftNIO SSH
本项目包含基于 [SwiftNIO](https://github.com/apple/swift-nio) 的 SSH 支持。
## SwiftNIO SSH 是什么?
SwiftNIO SSH 是 SSH 的一种编程式实现:也就是说,它是一组 API 集合,允许程序员实现能够说 SSH 协议的端点。关键在于,这意味着它更像 libssh2 而不是 openssh。SwiftNIO SSH 并不提供可直接用于生产的 SSH 客户端和服务器,而是提供构建此类客户端和服务器的基础构件。
提供编程式 SSH 实现有很多理由。其中之一是 SSH 与用户交互性有着独特的关系。技术用户非常习惯于以交互方式使用 SSH,无论是在远程机器上运行命令还是运行交互式 shell。具备以编程方式响应这些请求的能力,可以带来有趣的替代交互模式。作为先例,我们可以提到 Twisted 的 Manhole,它使用 [一个名为 `conch` 的编程式 SSH 实现](https://twistedmatrix.com/trac/wiki/TwistedConch) 在运行中的 Python 服务器内提供交互式 Python 解释器;还有 [ssh-chat](https://github.com/shazow/ssh-chat),一个提供聊天室而非常规 SSH shell 功能的 SSH 服务器。在 TCP 转发方面也可以设想出创新性的用途。
提供编程式 SSH 的另一个很好的理由是,服务之间以涉及运行命令的方式进行交互并不少见。虽然 `Process` 解决了本地场景下的需求,但有时需要调用的命令位于远程。虽然 `Process` 可以启动一个 `ssh` 客户端作为子进程来执行这种调用,但直接调用 SSH 本身显然要简单得多。这正是 [`libssh2`](https://www.libssh2.org) 的目标用例。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 ..< 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
## SwiftNIO SSH 支持什么?
SwiftNIO SSH 支持 SSHv2,并具有以下功能集:
- 所有会话通道功能,包括 shell 和 exec 通道请求
- 直接与反向 TCP 端口转发
- 仅使用现代密码学原语:非对称加密使用 Ed25519 和基于主要 NIST 曲线(P256、P384、P521)的 ECDSA,对称加密使用 AES-GCM,密钥交换使用 x25519
- 密码和公钥用户认证
- 支持 SwiftNIO 和 Swift Crypto 所支持的所有平台
## 如何使用 SwiftNIO SSH?
SwiftNIO SSH 提供一个 SwiftNIO `ChannelHandler`,即 `NIOSSHHandler`。该 handler 直接实现了 SSH 协议的大部分内容。用户不需要直接生成 SSH 消息;相反,他们通过子通道(child channels)和委托(delegates)与 `NIOSSHHandler` 进行交互。
SSH 是一种多路复用协议:每个 SSH 连接被细分为多个双向通信通道,恰如其分地称为“通道”(channels)。SwiftNIO SSH 通过使用“子通道”(child channel)抽象来反映这种构造。当对端创建一个新的 SSH 通道时,SwiftNIO SSH 会创建一个新的 NIO `Channel`,用于表示该 SSH 通道上的所有流量。在这个子 `Channel` 内,所有事件彼此之间是严格有序的;但是,不同 `Channel` 中的事件可以由实现自由地交错。
因此,一个活动的 SSH 连接看起来像这样:
```
┌ ─ NIO Channel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌────────────────────────────────┐ │
│ │
│ │ │ │
│ │
│ │ │ │
│ NIOSSHHandler │───────────────────────┐
│ │ │ │ │
│ │ │
│ │ │ │ │
│ │ │
│ └────────────────────────────────┘ │ │
│
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│
│
│
│
▼
┌── SSH Child Channel ─────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ├───┐
│ │ │ │ │ │ │
│ │ │ │ │ │ ├───┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ User Handler │ │ User Handler │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────┘ └────────────────────────────────┘ │ │ │
│ │ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
```
SSH 通道通过通道类型(channel type)来调用。NIOSSH 支持三种类型:`session`、`directTCPIP` 和 `forwardedTCPIP`。最常见的通道类型是 `session`:`session` 用于表示某个程序的调用,无论是特定命名的程序还是 shell。另外两种通道类型与 TCP 端口转发相关,将在后面讨论。
SSH 通道基于单一数据类型进行操作:`SSHChannelData`。该结构封装了 SSH 同时支持常规和“扩展”通道数据这一事实。常规通道数据(`SSHChannelData.DataType.channel`)用于绝大部分核心数据。在 `session` 通道中,`.channel` 数据类型用于标准输入和标准输出:`.stdErr` 数据类型则用于标准错误(理所当然)。在 TCP 转发通道中,`.channel` 数据类型是唯一使用的类型,表示被转发数据。
### 通道事件
`session` 通道表示对一条命令的调用。通道具体如何运作,通过若干入站用户事件(inbound user events)来传达。以下事件很重要:
- `SSHChannelRequestEvent.PseudoTerminalRequest`:请求分配一个伪终端。
- `SSHChannelRequestEvent.EnvironmentRequest`:为命令调用请求一个环境变量。总是在命令本身之前发送。
- `SSHChannelRequestEvent.ShellRequest`:请求将已认证用户的 shell 作为要调用的命令。
- `SSHChannelRequestEvent.ExecRequest`:请求调用特定命令。
- `SSHChannelRequestEvent.ExitStatus`:用于通知远程命令已退出,并传递退出码。
- `SSHChannelRequestEvent.ExitSignal`:用于指示远程命令因某个信号而终止,以及该信号是什么。
- `SSHChannelRequestEvent.SignalRequest`:用于向远程命令发送信号。
- `SSHChannelRequestEvent.LocalFlowControlRequest`:用于指示客户端是否能够自行执行 Ctrl-Q/Ctrl-S 流控制。
- `SSHChannelRequestEvent.WindowChangeRequest`:用于将客户端终端窗口大小的变化传达给已分配的伪终端。
- `SSHChannelRequestEvent.SubsystemRequest`:用于请求调用特定子系统。其含义因具体用例而异。
这些事件在端口转发消息中不会使用。支持 `.session` 类型通道的 SSH 实现需要准备好以各种方式处理其中大部分或全部事件。
这些事件也都有一个 `wantReply` 字段。它指示该请求是否需要回复来表示成功或失败。如果需要,则会使用以下两个事件:
- `ChannelSuccessEvent`,用于传达成功。
- `ChannelFailureEvent`,用于传达失败。
### 半关闭
SSH 网络协议在子通道中普遍使用半关闭。NIO `Channel` 默认情况下通常禁用半关闭支持,SwiftNIO SSH 在其子通道中也遵循这一默认设置。但是,如果你将此设置保留为默认值,SSH 子通道的行为将极其出人意料。因此,强烈建议为所有子通道启用半关闭支持:
```swift
channel.setOption(ChannelOptions.allowRemoteHalfClosure, true)
```
然后这会使用标准的 NIO 半关闭支持。远端发送 EOF 将通过一个入站用户事件 `ChannelEvent.inputClosed` 来传达。若想自己发送 EOF,请调用 `close(mode: .output)`。
### 用户认证
用户认证是 SSH 的关键组成部分。为了管理它,SwiftNIO SSH 使用一对委托协议:`NIOSSHClientUserAuthenticationDelegate` 和 `NIOSSHServerUserAuthenticationDelegate`。客户端和服务器应提供这些委托协议的实现来管理用户认证。
客户端协议很直接:SwiftNIO SSH 会在委托上调用 `nextAuthenticationType(availableMethods:nextChallengePromise:)` 方法。`availableMethods` 将是 `NIOSSHAvailableUserAuthenticationMethods` 的一个实例,用于传达服务器建议可接受的认证方法。然后,委托可以用一个新的认证请求来兑现 `nextChallengePromise`,或者用 `nil` 表示客户端已经没有可尝试的方法。
服务器协议则更复杂。委托必须提供一个 `supportedAuthenticationMethods` 属性,用于传达该委托支持哪些认证方法。然后,每次客户端发送用户认证请求时,都会调用 `requestReceived(request:responsePromise:)` 方法。由于允许客户端并行发出认证请求,因此该方法可能会被并行调用多次。`responsePromise` 应当以认证结果来成功完成。结果有三种:`.success` 和 `.failure` 直截了当,但原则上服务器可以使用 `.partialSuccess(remainingMethods:)` 要求多次挑战。
### 直接端口转发
直接端口转发是从客户端到服务器的端口转发。在这种模式下,传统上客户端会监听一个本地端口,并将入站连接转发到服务器。它会请求服务器将这些连接作为出站连接转发到特定的主机和端口。
客户端可以使用 `.directTCPIP` 通道类型直接打开这些通道。
### 远程端口转发与全局请求
远程端口转发是一种不太常见的情况,即客户端要求服务器监听特定地址和端口,并将所有入站连接转发给客户端。由于客户端需要请求此行为,因此它通过全局请求(global requests)来实现。
全局请求通过 `NIOSSHHandler.sendGlobalRequest` 发起,并由 `GlobalRequestDelegate` 接收和处理。目前支持两种全局请求:
- `GlobalRequest.TCPForwardingRequest.listen(host:port:)`:请求服务器在给定主机和端口上监听。
- `GlobalRequest.TCPForwardingRequest.cancel(host:port:)`:请求取消在给定主机和端口上的监听。
服务器可以通过 `GlobalRequestDelegate` 被通知并响应这些请求。这里要实现的方法是 `tcpForwardingRequest(_:handler:promise:)`。任何时候收到全局请求,都会调用此委托方法。对该请求的响应会传入 `promise`。
然后,转发通道通过 `.forwardedTCPIP` 通道类型从服务器发送到客户端。