
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 구현을 제공하는 데는 여러 이유가 있습니다. 그 중 하나는 SSH가 사용자 상호작용성과 독특한 관계를 가지고 있다는 점입니다. 기술 사용자들은 원격 머신에서 명령을 실행하거나 대화형 셸을 실행하기 위해 SSH와 대화형으로 상호작용하는 데 매우 익숙합니다. 이러한 요청에 프로그래매틱으로 응답할 수 있는 능력은 흥미로운 대안적 상호작용 방식을 가능하게 합니다. 이전 예로, Twisted의 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의 네트워킹 및 암호화 계층과 동등한 기능을 제공하므로, 의욕적인 사용자가 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를 지원합니다:
- 모든 세션 채널 기능(셸 및 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`. 가장 일반적인 채널 유형은 `session`입니다. `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`: 특정 하위 시스템의 호출을 요청하는 데 사용됩니다. 이 의미는 개별 사용 사례에 따라 다릅니다.
이 이벤트는 포트 포워딩 메시지에서 사용되지 않습니다. `.session` 유형 채널을 지원하는 SSH 구현은 다양한 방식으로 이러한 이벤트의 대부분 또는 전체를 처리할 준비가 되어 있어야 합니다.
이러한 각 이벤트에는 `wantReply` 필드도 있습니다. 이는 요청이 성공 또는 실패를 나타내는 응답이 필요한지 여부를 나타냅니다. 필요한 경우 다음 두 이벤트가 사용됩니다:
- `ChannelSuccessEvent`, 성공을 전달합니다.
- `ChannelFailureEvent`, 실패를 전달합니다.
### 반 폐쇄 (Half Closure)
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` 채널 유형을 사용하여 서버에서 클라이언트로 전송됩니다.