
SwiftNIO SSH — это программная реализация SSH с использованием SwiftNIO
# SwiftNIO SSH
Этот проект содержит поддержку SSH с использованием [SwiftNIO](https://github.com/apple/swift-nio).
## Что такое SwiftNIO SSH?
SwiftNIO SSH — это программная реализация SSH: то есть набор API, позволяющих программистам создавать конечные точки, работающие по протоколу SSH. Важно отметить, что это скорее аналог libssh2, нежели openssh. SwiftNIO SSH не предоставляет готовых к использованию в продакшене SSH-клиентов и серверов, а вместо этого предлагает строительные блоки для создания таких клиентов и серверов.
Существует несколько причин для создания программной реализации SSH. Одна из них — уникальное отношение SSH к интерактивности пользователя. Технические пользователи привыкли взаимодействовать с SSH интерактивно: либо для выполнения команд на удалённых машинах, либо для запуска интерактивных оболочек. Возможность программно отвечать на такие запросы открывает интересные альтернативные способы взаимодействия. В качестве примеров можно привести Twisted's Manhole, который использует [программную реализацию SSH под названием `conch`](https://twistedmatrix.com/trac/wiki/TwistedConch) для предоставления интерактивного интерпретатора Python внутри работающего Python-сервера, или [ssh-chat](https://github.com/shazow/ssh-chat) — SSH-сервер, который предоставляет чат-комнату вместо обычной функциональности SSH-оболочки. Также можно представить инновационные применения для пересылки TCP.
Ещё одна веская причина для создания программной реализации SSH — это то, что сервисам часто требуется взаимодействовать с другими сервисами путём выполнения команд. Хотя `Process` решает эту задачу для локального сценария, иногда команды нужно выполнять удалённо. Хотя `Process` может запустить клиент `ssh` как дочерний процесс для выполнения такого вызова, зачастую гораздо проще напрямую вызывать SSH. Это и является целевым сценарием использования [`libssh2`](https://www.libssh2.org). SwiftNIO SSH предоставляет эквивалент сетевого и криптографического уровня libssh2, позволяя мотивированным пользователям управлять SSH-сессиями напрямую из Swift-сервисов.
Последние версии SwiftNIO SSH поддерживают Swift 5.9 и новее. Минимальные поддерживаемые версии Swift для разных релизов SwiftNIO SSH указаны ниже:
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 и ECDSA по основным кривым NIST (P256, P384, P521) для асимметричной криптографии, AES-GCM для симметричной криптографии, x25519 для обмена ключами
- Аутентификация пользователя по паролю и с помощью открытого ключа
- Поддержка всех платформ, поддерживаемых SwiftNIO и Swift Crypto
## Как использовать SwiftNIO SSH?
SwiftNIO SSH предоставляет обработчик канала SwiftNIO — `NIOSSHHandler`. Этот обработчик реализует основную часть протокола 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 поддерживает три типа: `session`, `directTCPIP` и `forwardedTCPIP`. Наиболее распространённый тип — `session`: он используется для представления вызова программы, будь то конкретная именованная программа или оболочка. Два других типа связаны с пересылкой 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`: Используется для запроса вызова конкретной подсистемы. Смысл этого запроса зависит от конкретного сценария.
Эти события не используются в сообщениях пересылки портов. Реализации SSH, поддерживающие каналы типа `.session`, должны быть готовы обрабатывать большинство или все эти события различными способами.
Каждое из этих событий также имеет поле `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`.
### Удалённая пересылка портов и глобальные запросы
Удалённая пересылка портов — менее распространённая ситуация, когда клиент просит сервер прослушивать конкретный адрес и порт и перенаправлять все входящие соединения клиенту. Поскольку клиенту нужно запросить такое поведение, он делает это с помощью глобальных запросов.
Глобальные запросы инициируются с помощью `NIOSSHHandler.sendGlobalRequest`, а принимаются и обрабатываются через `GlobalRequestDelegate`. На данный момент поддерживаются два глобальных запроса:
- `GlobalRequest.TCPForwardingRequest.listen(host:port:)`: запрос на прослушивание сервером заданного хоста и порта.
- `GlobalRequest.TCPForwardingRequest.cancel(host:port:)`: запрос на отмену прослушивания заданного хоста и порта.
Серверы могут быть уведомлены об этих запросах и отвечать на них с помощью `GlobalRequestDelegate`. Реализуемый метод — `tcpForwardingRequest(_:handler:promise:)`. Этот метод делегата будет вызываться каждый раз, когда получен глобальный запрос. Ответ на запрос передаётся в `promise`.
Затем пересылаемые каналы отправляются от сервера к клиенту с использованием типа канала `.forwardedTCPIP`.