
SwiftNIO SSHは、SwiftNIOを使用したSSHのプログラム的な実装です。
# SwiftNIO SSH
このプロジェクトは、[SwiftNIO](https://github.com/apple/swift-nio) を使用したSSHサポートを含んでいます。
## SwiftNIO SSHとは?
SwiftNIO SSHは、SSHのプログラムによる実装です。つまり、プログラマがSSHを話すエンドポイントを実装できるようにするAPIのコレクションです。重要なのは、これはopensshというよりlibssh2に近いということです。SwiftNIO SSHは本番環境ですぐ使えるSSHクライアントやサーバを提供するのではなく、そのようなクライアントとサーバを構築するための構成要素を提供します。
プログラムによるSSH実装を提供する理由はいくつかあります。1つは、SSHがユーザーとの対話性に対して独自の関係を持っていることです。技術的なユーザーは、リモートマシンでコマンドを実行するためであれ、対話型シェルを実行するためであれ、SSHと対話的にやり取りすることに非常に慣れています。これらの要求にプログラム的に応答できることで、興味深い代替的な対話モードが可能になります。以前の例としては、実行中のPythonサーバ内で対話型Pythonインタープリタを提供するために[`conch`と呼ばれるプログラムによるSSH実装](https://twistedmatrix.com/trac/wiki/TwistedConch)を使用するTwistedのManholeや、通常のSSHシェル機能の代わりにチャットルームを提供するSSHサーバである[ssh-chat](https://github.com/shazow/ssh-chat)を挙げることができます。TCPフォワーディングについても革新的な用途が考えられます。
プログラムによるSSHを提供するもう1つの良い理由は、サービスがコマンドの実行を伴う形で他のサービスとやり取りする必要があることが珍しくないからです。`Process`はローカルでのユースケースを解決しますが、呼び出す必要があるコマンドがリモートにある場合もあります。`Process`はこの呼び出しを実行するために`ssh`クライアントをサブプロセスとして起動できますが、SSHを直接呼び出す方がはるかに簡単な場合があります。これは[`libssh2`](https://www.libssh2.org)のターゲットユースケースです。SwiftNIO SSHはlibssh2のネットワーキングおよび暗号化レイヤに相当するものを提供し、意欲的なユーザーがSwiftサービス内から直接SSHセッションを操作できるようにします。
SwiftNIO SSHの最新バージョンはSwift 5.9以降をサポートしています。SwiftNIO SSHのリリースがサポートする最小Swiftバージョンは以下のとおりです:
SwiftNIO SSH | Minimum Swift Version
--------------------|----------------------
`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をサポートしています:
- シェルおよびexecチャネルリクエストを含む、すべてのセッションチャネル機能
- 直接および逆方向のTCPポートフォワーディング
- 最新の暗号プリミティブのみ:非対称暗号には主要なNIST曲線(P256、P384、P521)上のEd25519およびECDSA、対称暗号にはAES-GCM、鍵交換にはx25519
- パスワードおよび公開鍵によるユーザー認証
- SwiftNIOとSwift Cryptoがサポートするすべてのプラットフォームをサポート
## SwiftNIO SSHはどのように使いますか?
SwiftNIO SSHは、SwiftNIOの`ChannelHandler`である`NIOSSHHandler`を提供します。このハンドラはSSHプロトコルの大部分を直接実装しています。ユーザーはSSHメッセージを直接生成する必要はありません。代わりに、子チャネルとデリゲートを通じて`NIOSSHHandler`とやり取りします。
SSHは多重化プロトコルです。各SSH接続は、適切にもチャネルと呼ばれる複数の双方向通信チャネルに分割されます。SwiftNIO SSHは「子チャネル」抽象化を使用してこの構造を反映します。ピアが新しいSSHチャネルを作成すると、SwiftNIO SSHはそのSSHチャネル上のすべてのトラフィックを表す新しいNIO `Channel`を作成します。この子`Channel`内では、すべてのイベントは互いに対して厳密に順序付けられます。ただし、異なる`Channel`のイベントは実装によって自由にインターリーブされる場合があります。
したがって、アクティブなSSH接続は次のようになります:
```
┌ ─ NIO Channel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌────────────────────────────────┐ │
│ │
│ │ │ │
│ │
│ │ │ │
│ NIOSSHHandler │───────────────────────┐
│ │ │ │ │
│ │ │
│ │ │ │ │
│ │ │
│ └────────────────────────────────┘ │ │
│
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│
│
│
│
▼
┌── SSH Child Channel ─────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ├───┐
│ │ │ │ │ │ │
│ │ │ │ │ │ ├───┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ User Handler │ │ User Handler │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────┘ └────────────────────────────────┘ │ │ │
│ │ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
```
SSHチャネルはチャネルタイプとともに呼び出されます。NIOSSHは`session`、`directTCPIP`、`forwardedTCPIP`の3つをサポートします。最も一般的なチャネルタイプは`session`です。`session`は、特定の名前付きプログラムであれシェルであれ、プログラムの呼び出しを表すために使用されます。他の2つのチャネルタイプはTCPポートフォワーディングに関連しており、後ほど説明します。
SSHチャネルは単一のデータ型`SSHChannelData`で動作します。この構造体は、SSHが通常のチャネルデータと「拡張」チャネルデータの両方をサポートしていることをカプセル化します。通常のチャネルデータ(`SSHChannelData.DataType.channel`)は、大部分のコアデータに使用されます。`session`チャネルでは、`.channel`データ型が標準入力と標準出力に使用され、`.stdErr`データ型が(当然ながら)標準エラーに使用されます。TCPフォワーディングチャネルでは、`.channel`データ型のみが使用され、フォワーディングされたデータを表します。
### チャネルイベント
`session`チャネルはコマンドの呼び出しを表します。チャネルがどのように動作するかは、いくつかのインバウンドユーザーイベントで伝えられます。以下のイベントが重要です:
- `SSHChannelRequestEvent.PseudoTerminalRequest`: 擬似端末の割り当てを要求します。
- `SSHChannelRequestEvent.EnvironmentRequest`: コマンド呼び出しのための単一の環境変数を要求します。常にコマンド自体より前に送信されます。
- `SSHChannelRequestEvent.ShellRequest`: 呼び出されるコマンドが認証されたユーザーのシェルであることを要求します。
- `SSHChannelRequestEvent.ExecRequest`: 特定のコマンドの呼び出しを要求します。
- `SSHChannelRequestEvent.ExitStatus`: リモートコマンドが終了したことを通知し、終了コードを伝えるために使用されます。
- `SSHChannelRequestEvent.ExitSignal`: リモートコマンドがシグナルに応答して終了したこと、およびそのシグナルが何であったかを示すために使用されます。
- `SSHChannelRequestEvent.SignalRequest`: リモートコマンドにシグナルを送信するために使用されます。
- `SSHChannelRequestEvent.LocalFlowControlRequest`: クライアントがCtrl-Q/Ctrl-Sフロー制御を自身で実行できるかどうかを示すために使用されます。
- `SSHChannelRequestEvent.WindowChangeRequest`: クライアントのターミナルウィンドウサイズの変更を、割り当てられた擬似端末に伝えるために使用されます。
- `SSHChannelRequestEvent.SubsystemRequest`: 特定のサブシステムの呼び出しを要求するために使用されます。この意味は個々のユースケースに固有です。
これらのイベントはポートフォワーディングメッセージでは使用されません。`.session`タイプのチャネルをサポートするSSH実装は、これらのほとんどまたはすべてをさまざまな方法で処理できるように準備する必要があります。
これらの各イベントには`wantReply`フィールドもあります。これは、リクエストが成功または失敗を示す応答を必要とするかどうかを示します。必要とする場合、次の2つのイベントが使用されます:
- `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は2つのデリゲートプロトコル、`NIOSSHClientUserAuthenticationDelegate`と`NIOSSHServerUserAuthenticationDelegate`を使用します。クライアントとサーバは、ユーザー認証を管理するためにこれらのデリゲートプロトコルの実装を提供する必要があります。
クライアントプロトコルは簡単です。SwiftNIO SSHはデリゲートの`nextAuthenticationType(availableMethods:nextChallengePromise:)`メソッドを呼び出します。`availableMethods`は、サーバが受け入れ可能と提案した認証方法を伝える`NIOSSHAvailableUserAuthenticationMethods`のインスタンスになります。デリゲートはその後、新しい認証リクエスト、またはクライアントが試す手段を使い果たしたことを示す`nil`で`nextChallengePromise`を完了できます。
サーバプロトコルはより複雑です。デリゲートは、デリゲートがサポートする認証方法を伝える`supportedAuthenticationMethods`プロパティを提供する必要があります。その後、クライアントがユーザー認証リクエストを送信するたびに、`requestReceived(request:responsePromise:)`メソッドが呼び出されます。クライアントは認証リクエストを並行して発行できるため、これは並行して複数回呼び出される場合があります。`responsePromise`は認証の結果で成功として完了させる必要があります。結果は3つあります:`.success`と`.failure`は単純ですが、原則としてサーバは`.partialSuccess(remainingMethods:)`を使用して複数のチャレンジを要求できます。
### 直接ポートフォワーディング
直接ポートフォワーディングは、クライアントからサーバへのポートフォワーディングです。このモードでは、従来クライアントがローカルポートで待ち受け、インバウンド接続をサーバに転送します。クライアントは、これらの接続を特定のホストとポートへのアウトバウンド接続として転送するようサーバに要求します。
クライアントは`.directTCPIP`チャネルタイプを使用してこれらのチャネルを直接開くことができます。
### リモートポートフォワーディングとグローバルリクエスト
リモートポートフォワーディングはあまり一般的ではない状況で、クライアントがサーバに特定のアドレスとポートで待ち受けるよう要求し、すべてのインバウンド接続をクライアントに転送します。クライアントはこの動作を要求する必要があるため、グローバルリクエストを使用して行います。
グローバルリクエストは`NIOSSHHandler.sendGlobalRequest`を使用して開始され、`GlobalRequestDelegate`を通じて受信および処理されます。現在サポートされているグローバルリクエストは2つあります:
- `GlobalRequest.TCPForwardingRequest.listen(host:port:)`: サーバが指定されたホストとポートで待ち受けるためのリクエスト。
- `GlobalRequest.TCPForwardingRequest.cancel(host:port:)`: 指定されたホストとポートでの待ち受けをキャンセルするためのリクエスト。
サーバは`GlobalRequestDelegate`を使用してこれらのリクエストの通知を受け、応答できます。ここで実装するメソッドは`tcpForwardingRequest(_:handler:promise:)`です。このデリゲートメソッドは、グローバルリクエストが受信されるたびに呼び出されます。リクエストへの応答は`promise`に渡されます。
転送されたチャネルは、その後`.forwardedTCPIP`チャネルタイプを使用してサーバからクライアントに送信されます。