Architecture

How SSHKit is structured, how it owns libssh state, how cancellation tears connections down, and the rules that hold across releases.

Three layers

CLibSSH       C        Vendored libssh + OpenSSL backend + zlib.
                       Public headers in Sources/CLibSSH/include.
   |
SSHKitObjC    Obj-C    Public API (SSHKit* prefix) + internal core (SSHCore* prefix).
                       Owns ssh_session, socket fd, worker queue, cancellation token.
   |
SSHKit        Swift    async/await + callback API, typed errors, Sendable value types.

All three products are dynamic libraries (.library(... type: .dynamic ...)) so the libssh LGPL boundary stays an explicit, swappable boundary for applications. The Swift module is the primary surface; the Obj-C module is callable from Obj-C applications directly.

Ownership invariants

These are load-bearing rules. Permanent — not subject to change between releases.

Threading

ObjectQueueSocketlibssh
SSHClientnonenonenone
SSHConnectionnonenoneSwift wrapper
SSHKitConnectionnonenoneObj-C wrapper
SSHCoreSessionWorkerone serial queueone fdone ssh_session
SSHShell / SSHCommand / SFTPClient / SSHTunnelChannelworker queuenoneone channel via worker

Every SSHConnection owns one SSHCoreSessionWorker. That worker has one serial dispatch queue. GCD schedules the queue onto a system thread when work is active — blocking libssh calls can occupy that worker thread until completion or cancellation.

Rules:

Cancellation & close

SSHKit uses blocking libssh I/O. To unblock a libssh call from another thread it shuts down the socket beneath libssh:

  1. The caller invokes cancel() or close() from any thread (Swift Task cancellation funnels through the same path).
  2. The public object marks the operation as cancelled or closing.
  3. SSHCoreSocketHandle.shutdownNow() calls shutdown(fd, SHUT_RDWR).
  4. Any blocking connect/read/write inside libssh wakes up.
  5. The worker queue maps the wake-up to cancellation or close.
  6. The worker queue performs libssh cleanup.
  7. The worker queue closes the fd through takeFileDescriptorForClose().

This is a full-connection teardown. Cancelling a single SFTP read or a single tunnel write tears the whole connection down — the underlying socket is gone and any other channel on the same session goes with it. To run the next operation, open a fresh SSHConnection.

The caller thread may call shutdownNow(); only the worker queue ever calls close(fd). This avoids fd-reuse hazards. shutdown(fd, SHUT_RDWR) may report EBADF, ENOTCONN, or similar already-closed states on the cancellation path — SSHKit records those as debug diagnostics and continues cleanup.

Connection state machine

idle
  -> connecting
  -> ready
  -> runningCommand
  -> ready
  -> runningShell
  -> ready
  -> runningSFTP
  -> ready
  -> runningTunnel
  -> ready
  -> closing
  -> closed

Only one running* state is active at any time. close() is idempotent. Other invalid transitions are programmer errors.

Current stateCallResult
connectingexecuteprogrammer failure
runningShellopenSFTPprogrammer failure
closingexecuteprogrammer failure
closedcloseidempotent success
closedopenShellprogrammer failure in debug, typed error in release

Fail-loud policy

Caller misuse is a programming bug and SSHKit makes it loud during development.

Runtime failures — network loss, authentication rejection, host-key mismatch, server disconnect, protocol errors, SFTP status mappings — are always typed errors, never crashes. Crashes are reserved for the things you can fix in your own code. Command non-zero exit is not an error: execute returns an SSHCommandResult with the exit status; inspect it.

Host trust storage

Host trust is explicit and injectable. SSHHostKeyPolicy covers the four ways to validate the server’s key:

Two trust stores ship with the package — SSHMemoryHostTrustStore and SSHKeychainHostTrustStore. The Keychain store keys entries by lowercase host:port and defaults to service name wiki.qaq.sshkit. Applications may inject any conforming SSHHostTrustStore.

Algorithm profiles

SSHAlgorithmProfile wraps libssh’s comma-separated algorithm lists.

libssh rejects unsupported algorithm names during session configuration and algorithm inspection; SSHKit exposes those failures as typed errors. publicKeyAcceptedAlgorithms inherits libssh’s host-key defaults when the profile leaves it unset. libssh exposes minimumRSAKeySize as a set-only option, so the inspection snapshot reports the configured value when a profile supplies one and leaves it absent otherwise. The set of accepted algorithms is bounded by what the vendored libssh + OpenSSL build supports; endpoints that need algorithms outside that set require server-side changes or a custom libssh build.

Permanent architecture rules

These rules will not change between SSHKit releases: