
SwiftNIO SSH ist eine programmatische Implementierung von SSH mit SwiftNIO.
# SwiftNIO SSH
Dieses Projekt enthält SSH-Unterstützung unter Verwendung von [SwiftNIO](https://github.com/apple/swift-nio).
## Was ist SwiftNIO SSH?
SwiftNIO SSH ist eine programmatische Implementierung von SSH: das heißt, es ist eine Sammlung von APIs, die es Programmierern ermöglichen, SSH-sprechende Endpunkte zu implementieren. Entscheidend ist, dass es eher wie libssh2 als wie OpenSSH ist. SwiftNIO SSH liefert keine produktionsreifen SSH-Clients und -Server, sondern bietet die Bausteine, um solche Clients und Server zu erstellen.
Es gibt eine Reihe von Gründen, eine programmatische SSH-Implementierung bereitzustellen. Einer ist, dass SSH eine einzigartige Beziehung zur Benutzerinteraktivität hat. Technische Benutzer sind es gewohnt, interaktiv mit SSH zu arbeiten, entweder um Befehle auf entfernten Maschinen auszuführen oder um interaktive Shells zu nutzen. Die Fähigkeit, programmatisch auf diese Anfragen zu reagieren, ermöglicht interessante alternative Interaktionsmodi. Als frühere Beispiele können wir Twisted's Manhole nennen, das [eine programmatische SSH-Implementierung namens `conch`](https://twistedmatrix.com/trac/wiki/TwistedConch) verwendet, um einen interaktiven Python-Interpreter in einem laufenden Python-Server bereitzustellen, oder [ssh-chat](https://github.com/shazow/ssh-chat), einen SSH-Server, der anstelle der normalen SSH-Shell-Funktionalität einen Chatroom bereitstellt. Innovative Verwendungen sind auch für die TCP-Weiterleitung denkbar.
Ein weiterer guter Grund, eine programmatische SSH-Lösung bereitzustellen, ist, dass es nicht ungewöhnlich ist, dass Dienste mit anderen Diensten interagieren müssen, indem sie Befehle ausführen. Während `Process` dies für den lokalen Anwendungsfall löst, müssen die auszuführenden Befehle manchmal entfernt liegen. Obwohl `Process` einen `ssh`-Client als Subprozess starten könnte, um diese Ausführung durchzuführen, kann es wesentlich einfacher sein, SSH direkt aufzurufen. Dies ist der Zielanwendungsfall von [`libssh2`](https://www.libssh2.org). SwiftNIO SSH bietet das Äquivalent der Netzwerk- und Kryptographieschicht von libssh2 und ermöglicht es motivierten Benutzern, SSH-Sitzungen direkt aus Swift-Diensten heraus zu steuern.
Die aktuellsten Versionen von SwiftNIO SSH unterstützen Swift 5.9 und neuer. Die minimale Swift-Version, die von den SwiftNIO SSH-Versionen unterstützt wird, ist unten im Detail aufgeführt:
SwiftNIO SSH | Minimale 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
## Was unterstützt SwiftNIO SSH?
SwiftNIO SSH unterstützt SSHv2 mit den folgenden Funktionen:
- Alle Session-Channel-Features, einschließlich Shell- und Exec-Channel-Anfragen
- Direkte und umgekehrte TCP-Portweiterleitung
- Nur moderne kryptografische Primitiven: Ed25519 und ECDSA über die wichtigsten NIST-Kurven (P256, P384, P521) für asymmetrische Kryptografie, AES-GCM für symmetrische Kryptografie, x25519 für den Schlüsselaustausch
- Passwort- und Public-Key-Benutzerauthentifizierung
- Unterstützt alle Plattformen, die von SwiftNIO und Swift Crypto unterstützt werden
## Wie verwende ich SwiftNIO SSH?
SwiftNIO SSH bietet einen SwiftNIO `ChannelHandler`, `NIOSSHHandler`. Dieser Handler implementiert den Großteil des SSH-Protokolls direkt. Benutzer werden nicht erwartet, SSH-Nachrichten direkt zu generieren: stattdessen interagieren sie mit dem `NIOSSHHandler` über Kind-Channels und Delegaten.
SSH ist ein multiplexiertes Protokoll: Jede SSH-Verbindung wird in mehrere bidirektionale Kommunikationskanäle unterteilt, die entsprechend als "Channels" bezeichnet werden. SwiftNIO SSH spiegelt diese Konstruktion wider, indem es eine Abstraktion von "Kind-Channels" verwendet. Wenn ein Peer einen neuen SSH-Channel erstellt, erstellt SwiftNIO SSH einen neuen NIO `Channel`, der den gesamten Datenverkehr auf diesem SSH-Channel repräsentiert. Innerhalb dieses Kind-`Channel` sind alle Ereignisse streng in Bezug zueinander geordnet: Ereignisse in verschiedenen `Channel`s können jedoch von der Implementierung frei verschachtelt werden.
Eine aktive SSH-Verbindung sieht daher wie folgt aus:
```
┌ ─ NIO Channel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌────────────────────────────────┐ │
│ │
│ │ │ │
│ │
│ │ │ │
│ NIOSSHHandler │───────────────────────┐
│ │ │ │ │
│ │ │
│ │ │ │ │
│ │ │
│ └────────────────────────────────┘ │ │
│
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│
│
│
│
▼
┌── SSH Child Channel ─────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ├───┐
│ │ │ │ │ │ │
│ │ │ │ │ │ ├───┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ User Handler │ │ User Handler │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────┘ └────────────────────────────────┘ │ │ │
│ │ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
```
Ein SSH-Channel wird mit einem Channel-Typ aufgerufen. NIOSSH unterstützt drei: `session`, `directTCPIP` und `forwardedTCPIP`. Der häufigste Channel-Typ ist `session`: `session` wird verwendet, um den Aufruf eines Programms darzustellen, sei es ein bestimmtes benanntes Programm oder eine Shell. Die anderen beiden Channel-Typen beziehen sich auf die TCP-Portweiterleitung und werden später besprochen.
Ein SSH-Channel arbeitet mit einem einzigen Datentyp: `SSHChannelData`. Diese Struktur kapselt die Tatsache, dass SSH sowohl normale als auch "erweiterte" Channel-Daten unterstützt. Die normalen Channel-Daten (`SSHChannelData.DataType.channel`) werden für die überwiegende Mehrheit der Kerndaten verwendet. In `session`-Channels wird der `.channel`-Datentyp für Standardeingabe und -ausgabe verwendet: der `.stdErr`-Datentyp wird für die Standardfehlerausgabe verwendet (natürlich). In TCP-Weiterleitungskanälen wird nur der `.channel`-Datentyp verwendet und repräsentiert die weitergeleiteten Daten.
### Kanalereignisse
Ein `session`-Channel repräsentiert den Aufruf eines Befehls. Wie genau der Channel arbeitet, wird in einer Reihe von eingehenden Benutzerereignissen mitgeteilt. Die folgenden Ereignisse sind wichtig:
- `SSHChannelRequestEvent.PseudoTerminalRequest`: Fordert die Zuweisung eines Pseudo-Terminals an.
- `SSHChannelRequestEvent.EnvironmentRequest`: Fordert eine einzelne Umgebungsvariable für den Befehlsaufruf an. Wird immer vor dem Befehl selbst gesendet.
- `SSHChannelRequestEvent.ShellRequest`: Fordert an, dass der Befehl, der ausgeführt werden soll, die Shell des authentifizierten Benutzers ist.
- `SSHChannelRequestEvent.ExecRequest`: Fordert den Aufruf eines bestimmten Befehls an.
- `SSHChannelRequestEvent.ExitStatus`: Wird verwendet, um zu signalisieren, dass der entfernte Befehl beendet wurde, und teilt den Exit-Code mit.
- `SSHChannelRequestEvent.ExitSignal`: Wird verwendet, um anzuzeigen, dass der entfernte Befehl als Reaktion auf ein Signal beendet wurde, und um welches Signal es sich handelt.
- `SSHChannelRequestEvent.SignalRequest`: Wird verwendet, um ein Signal an den entfernten Befehl zu senden.
- `SSHChannelRequestEvent.LocalFlowControlRequest`: Wird verwendet, um anzuzeigen, ob der Client in der Lage ist, die Ctrl-Q/Ctrl-S-Flusskontrolle selbst durchzuführen.
- `SSHChannelRequestEvent.WindowChangeRequest`: Wird verwendet, um eine Änderung der Größe des Terminalfensters auf dem Client an das zugewiesene Pseudo-Terminal zu übermitteln.
- `SSHChannelRequestEvent.SubsystemRequest`: Wird verwendet, um den Aufruf eines bestimmten Subsystems anzufordern. Die Bedeutung ist anwendungsspezifisch.
Diese Ereignisse werden in Portweiterleitungsnachrichten nicht verwendet. SSH-Implementierungen, die `.session`-Typ-Channels unterstützen, müssen darauf vorbereitet sein, die meisten oder alle dieser Ereignisse auf verschiedene Weise zu behandeln.
Jedes dieser Ereignisse hat auch ein `wantReply`-Feld. Dies zeigt an, ob die Anfrage eine Antwort benötigt, um Erfolg oder Misserfolg anzuzeigen. Wenn ja, werden die folgenden beiden Ereignisse verwendet:
- `ChannelSuccessEvent`, um Erfolg zu melden.
- `ChannelFailureEvent`, um Misserfolg zu melden.
### Halbschließung
Das SSH-Netzwerkprotokoll verwendet in den Kind-Channels durchgängig Halbschließung. NIO `Channel`s haben standardmäßig die Halbschließungsunterstützung deaktiviert, und SwiftNIO SSH respektiert diese Voreinstellung auch in seinen Kind-Channels. Wenn Sie diese Einstellung jedoch auf dem Standardwert belassen, verhalten sich die SSH-Kind-Channels äußerst unerwartet. Daher wird dringend empfohlen, dass alle Kind-Channels die Halbschließungsunterstützung aktivieren:
```swift
channel.setOption(ChannelOptions.allowRemoteHalfClosure, true)
```
Dies verwendet dann die standardmäßige NIO-Halbschließungsunterstützung. Der entfernte Peer, der EOF sendet, wird mit einem eingehenden Benutzerereignis, `ChannelEvent.inputClosed`, mitgeteilt. Um selbst EOF zu senden, rufen Sie `close(mode: .output)` auf.
### Benutzerauthentifizierung
Die Benutzerauthentifizierung ist ein wesentlicher Bestandteil von SSH. Zur Verwaltung verwendet SwiftNIO SSH ein Paar von Delegatprotokollen: `NIOSSHClientUserAuthenticationDelegate` und `NIOSSHServerUserAuthenticationDelegate`. Clients und Server sollten Implementierungen dieser Delegatprotokolle bereitstellen, um die Benutzerauthentifizierung zu verwalten.
Das Client-Protokoll ist einfach: SwiftNIO SSH ruft die Methode `nextAuthenticationType(availableMethods:nextChallengePromise:)` auf dem Delegaten auf. Die `availableMethods` sind eine Instanz von `NIOSSHAvailableUserAuthenticationMethods`, die mitteilt, welche Authentifizierungsmethoden der Server als akzeptabel vorgeschlagen hat. Der Delegat kann dann das `nextChallengePromise` entweder mit einer neuen Authentifizierungsanfrage oder mit `nil` abschließen, um anzuzeigen, dass dem Client nichts mehr zum Ausprobieren einfällt.
Das Server-Protokoll ist komplexer. Der Delegat muss eine `supportedAuthenticationMethods`-Eigenschaft bereitstellen, die mitteilt, welche Authentifizierungsmethoden vom Delegaten unterstützt werden. Dann wird jedes Mal, wenn der Client eine Benutzerauthentifizierungsanfrage sendet, die Methode `requestReceived(request:responsePromise:)` aufgerufen. Diese kann mehrmals parallel aufgerufen werden, da Clients berechtigt sind, Authentifizierungsanfragen parallel zu stellen. Das `responsePromise` sollte mit dem Ergebnis der Authentifizierung erfolgreich abgeschlossen werden. Es gibt drei Ergebnisse: `.success` und `.failure` sind einfach, aber im Prinzip kann der Server mehrere Herausforderungen mit `.partialSuccess(remainingMethods:)` verlangen.
### Direkte Portweiterleitung
Direkte Portweiterleitung ist die Portweiterleitung vom Client zum Server. In diesem Modus hört der Client traditionell auf einem lokalen Port und leitet eingehende Verbindungen an den Server weiter. Er bittet den Server, diese Verbindungen als ausgehende Verbindungen zu einem bestimmten Host und Port weiterzuleiten.
Diese Channels können von Clients direkt mit dem Channel-Typ `.directTCPIP` geöffnet werden.
### Remote-Portweiterleitung und globale Anfragen
Remote-Portweiterleitung ist eine weniger häufige Situation, in der der Client den Server bittet, auf einer bestimmten Adresse und einem bestimmten Port zu lauschen und alle eingehenden Verbindungen an den Client weiterzuleiten. Da der Client dieses Verhalten anfordern muss, tut er dies mit globalen Anfragen.
Globale Anfragen werden mit `NIOSSHHandler.sendGlobalRequest` initiiert und über einen `GlobalRequestDelegate` empfangen und verarbeitet. Heute werden zwei globale Anfragen unterstützt:
- `GlobalRequest.TCPForwardingRequest.listen(host:port:)`: eine Anfrage, dass der Server auf einem bestimmten Host und Port lauscht.
- `GlobalRequest.TCPForwardingRequest.cancel(host:port:)`: eine Anfrage, das Lauschen auf dem bestimmten Host und Port zu beenden.
Server können über einen `GlobalRequestDelegate` über diese Anfragen benachrichtigt werden und darauf antworten. Die zu implementierende Methode ist `tcpForwardingRequest(_:handler:promise:)`. Diese Delegatenmethode wird jedes Mal aufgerufen, wenn eine globale Anfrage empfangen wird. Die Antwort auf die Anfrage wird in `promise` übergeben.
Weitergeleitete Channels werden dann mit dem Channel-Typ `.forwardedTCPIP` vom Server zum Client gesendet.