November 19, 2020·5 min read·engineeringswiftsshnetworkingservers

SwiftNIO SSH exposes channels and requests to application code

SwiftNIO SSH provides protocol components for custom endpoints, including multiplexed channels, asynchronous authentication and port-forwarding requests.

An SSH endpoint can offer an interaction model defined by an application, without being a general login service. A remote command or a forwarded connection can be useful inside another service's own workflow. SwiftNIO SSH, announced on November 19, supplies protocol-level building blocks for such endpoints. It does not ship a production-ready command-line client or server.

The distinction establishes who implements the application behavior. SwiftNIO SSH handles networking and cryptography, while its consumer decides how to authenticate users and respond to channel requests. The launch describes a prerelease considered feature complete, with public experience still desired before a stable release. That is a library adoption decision with a specific scope, rather than a replacement command to install on a server.

I find Swift's explicit types useful when a network protocol has several distinct operations. An API that preserves those distinctions can make an endpoint's behavior easier to inspect. A request to execute a command and a request to create a listener should remain different decisions in application code, even when both arrive over one encrypted connection.

One connection contains multiple child channels

The repository documentation available at launch describes NIOSSHHandler as the main protocol handler. Applications interact with it through delegates and NIO child channels. A child channel represents one bidirectional SSH channel inside the larger connection.

Events within a child channel are ordered relative to one another. Events belonging to different child channels can interleave. The distinction allows independent interactions over the same connection without inventing one global ordering for their application events. An endpoint should keep the state associated with a particular channel attached to that interaction.

The supported channel types are session, directTCPIP and forwardedTCPIP. A session represents a command or shell interaction. The other types express the two directions of TCP forwarding. This gives an implementation a typed place to decide which capabilities its endpoint offers.

Channel payloads use SSHChannelData. Session input and output use ordinary channel data, while standard error uses the extended-data form represented by .stdErr. Forwarded TCP data uses the ordinary form. Preserving that distinction lets a client handle command output and diagnostics without pretending they are one undifferentiated byte stream.

A session has requests beyond its byte stream

A session channel communicates behavior through user events. ExecRequest asks for a specific command, while ShellRequest asks for the authenticated user's shell. A pseudo-terminal request changes the interaction again. These requests are part of the protocol and need application handling.

Environment requests arrive before the command they concern. Window-change requests can update the dimensions of an allocated terminal. Exit status and exit signal communicate different reasons why a remote command has finished. An implementation supporting interactive sessions consequently needs more than a handler that copies incoming bytes to a subprocess.

The request's wantReply field indicates whether the peer expects success or failure to be reported. The matching events are ChannelSuccessEvent and ChannelFailureEvent. Ignoring the reply requirement would leave a peer waiting for a protocol response even if the application had already decided what to do.

This event model is useful for constrained endpoints. A service can handle the requests meaningful to its interaction and reject unsupported ones. The library exposes the protocol decision points, while the application remains responsible for giving them coherent behavior.

End-of-input does not mean all output is finished

The README calls out half closure as an important integration detail. SSH commonly allows one direction of a child channel to finish while the other remains active. NIO child channels inherit a default with half-closure support disabled, so the documentation strongly recommends enabling it.

The relevant option can be set on a child channel during its setup. Enabling it preserves the channel's two directional lifetimes:

channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true)

A remote EOF then appears as ChannelEvent.inputClosed. Sending EOF uses close(mode: .output). The endpoint can stop sending input without throwing away output still coming back from the other side.

A remote command illustrates the need. Its standard input can finish before the command produces its final output and exit status. Treating input closure as immediate full-channel destruction would collapse those distinct stages. The protocol adapter has to preserve the lifecycle of both directions.

Authentication delegates can complete asynchronously

The client authentication delegate receives the methods the server says are available. It completes a promise with another authentication request or with nil when it has no further method to try. This makes retry selection a client policy expressed through the delegate.

The server delegate declares supported methods and handles individual requests through a response promise. Responses include success and failure, with partial success available when additional challenges remain. The documentation notes that clients may send authentication requests in parallel, so the delegate cannot assume there is only one pending request at a time.

Asynchronous completion matters when the authentication decision needs information unavailable immediately in memory. A delegate can perform its own lookup and complete the promise later. The protocol implementation does not need to know how that lookup is organized, but the delegate must still handle its own concurrent requests correctly.

I prefer that separation because it keeps identity policy outside the cryptographic transport implementation. The endpoint's authentication rules are application behavior. Reusing SSH should make that behavior explicit through a supported interface, rather than burying it in packet construction.

Forwarding uses channel and connection scope

Direct forwarding lets the client ask the server to connect to a destination host and port through a directTCPIP channel. Remote forwarding first asks the server to create a listener. That request affects the connection and uses a global request, rather than belonging to one existing child channel.

NIOSSHHandler.sendGlobalRequest initiates such requests, and a GlobalRequestDelegate handles them. The supported forwarding requests create or cancel a listener. Once the listener exists, incoming connections reach the client through forwardedTCPIP channels.

The distinction gives a custom endpoint separate control over listener creation and individual forwarded streams. A listener can outlive one forwarded connection, so treating both as one channel operation would lose the relationship the protocol describes. The delegate and child-channel interfaces preserve those separate lifetimes for the application implementing the endpoint.