Traversio

Quickstart

Add Traversio to a Swift package and open your first SSH connection.

Before You Start

The package declares its public platform floor directly:

  • macOS 10.15
  • iOS 13
  • tvOS 13
  • watchOS 6
  • visionOS 1

At runtime, Traversio still prefers the newer Apple transport APIs on platform release 26 and later. Older supported releases automatically use the compatibility transport and listener backends behind the same public API surface.

Other practical notes:

  • Traversio targets Apple platforms first.
  • The package uses swift-tools-version: 6.2.
  • SSHClientLogHandler.osLog(...) requires the newer OSLog availability level on each platform. The core SSH API does not.

Traversio 1.0.x documents the current supported workflows. Validate those workflows against your own servers, proxy routes, and recovery policy before making it the default SSH engine for critical workloads.

Read Mental Model first for object graph context before code examples.

Add the Package

Add Traversio through the current 1.0.3 release line:

// Package.swift
dependencies: [
    .package(
        url: "https://github.com/GitSwiftHQ/Traversio.git",
        from: "1.0.3"
    )
]

Then add the library to your target:

// Package.swift
.target(
    name: "ExampleApp",
    dependencies: [
        .product(name: "Traversio", package: "Traversio")
    ]
)

First Traversio Connection

The public API has two connection shapes:

  • SSHClient.connect(configuration:) for explicit long-lived ownership
  • SSHClient.withConnection(configuration:_:) as a convenience wrapper which closes the connection automatically

For the first integration pass, keep the shape small: describe the server and host-trust policy in one Swift value, open a closure-scoped connection, run a command, and decode the output.

import Traversio

func runRemoteUname(secret: String) async throws -> String {
    let configuration = SSHClientConfiguration(
        host: "demo.traversio.example",
        username: "traversio",
        authentication: .password(secret),
        hostKeyPolicy: .knownHostsFile("/Users/me/.ssh/known_hosts")
    )

    let result = try await SSHClient.withConnection(configuration: configuration) { connection in
        try await connection.execute("echo traversio")
    }

    return String(decoding: result.standardOutput, as: UTF8.self)
}

This example shows the Traversio connection model:

  • SSHClientConfiguration owns endpoint, authentication, and host trust in one value.
  • withConnection(...) keeps the SSH connection scoped to the operation and closes it on success or failure.
  • execute(...) returns structured stdout, stderr, and exit-status data instead of asking the app to manage raw channel state.

Use SSHClient.connect(configuration:) when explicit long-lived ownership fits your flow. Close that connection yourself when the app is done with all child sessions, SFTP clients, or forwarding scopes.

Connection Flow

Within one connection call, Traversio performs:

  1. TCP connection setup through the selected transport backend. Apple 26+ systems prefer the newer backend, and older supported releases use the compatibility backend automatically.
  2. SSH identification exchange.
  3. Curve25519 or NIST ECDH key exchange and encrypted transport activation.
  4. Host-key trust evaluation using the configured policy.
  5. User authentication.
  6. Return of a live SSHConnection, or execution of your withConnection(...) body with that same wrapper.

If you use withConnection(...), the connection is closed automatically when the closure returns or throws.

Transport Options

The default client profile keeps compression off, uses the current automatic rekey defaults, leaves keepalive disabled, applies a 30-second connection setup timeout, and allows 120 seconds for host-key trust confirmation. Reply timeouts stay unbounded until configured.

Configure RFC 4253 zlib, delayed OpenSSH compression, tighter client-initiated rekey thresholds, post-auth idle keepalive, or bounded waits for reply paths explicitly on SSHClientConfiguration:

import Traversio

func runCompressedSession() async throws {
    let configuration = SSHClientConfiguration(
        host: "example.com",
        username: "deploy",
        authentication: .password("secret"),
        hostKeyPolicy: .knownHostsFile("/Users/me/.ssh/known_hosts"),
        compressionPreference: .delayedZlib,
        automaticRekeyPolicy: .init(
            outboundPacketThreshold: 250_000,
            inboundPacketThreshold: 250_000,
            idleTimeInterval: 600
        ),
        keepalivePolicy: .init(interval: 30),
        timeoutPolicy: .init(
            connectionSetupTimeInterval: 15,
            hostKeyTrustTimeInterval: 120,
            responseTimeInterval: 5
        )
    )

    try await SSHClient.withConnection(configuration: configuration) { connection in
        _ = try await connection.execute("hostname")
    }
}

That example enables:

  • delayed OpenSSH compression through .delayedZlib; use .zlib instead when the target explicitly advertises RFC 4253 zlib
  • tighter packet-threshold plus idle-time automatic rekey
  • a 30-second keepalive for idle authenticated connections
  • an explicit 15-second connection setup timeout, 120-second host-key trust timeout, and 5-second prompt reply timeout

For the full field-by-field guide, including defaults, tradeoffs, proxy settings, and how keepalivePolicy differs from timeoutPolicy, see Connection Configuration.

Structured Logging

If the app should record connection setup, authentication, and wrapped operation failures, use the logHandler: overloads:

import Traversio

func runLoggedCommand(configuration: SSHClientConfiguration) async throws {
    let recorder = SSHClientLogRecorder(maximumEventCount: 80)
    let logHandler = recorder.logHandler(
        minimumLevel: .info
    )

    try await SSHClient.withConnection(
        configuration: configuration,
        logHandler: logHandler
    ) { connection in
        _ = try await connection.execute("uptime")
    }
}

For a bounded recent-event buffer that can be exported with support data, attach SSHClientLogRecorder through diagnostics.logHandler(minimumLevel:) or SSHClientLogHandler.recorder(...).

If your deployment target also supports the newer OSLog helper overloads, SSHClientLogHandler.osLog(...) remains available as a convenience adapter.

If you already have your own logging pipeline, use SSHClientLogHandler.sink(...) instead and forward each SSHClientLogEvent yourself.

SSHClientLogEvent also exposes formattedLine, and SSHConnectionFailure / SSHOperationFailure each expose diagnosticReport for copyable support text.

For a reusable app-side pattern, see Diagnostics. That page shows the built-in bounded recorder, a one-tap export flow, and the data you can expect to include when a user reports that a host cannot be reached.

Lifetime Rules

These public wrappers stay valid while their owning SSHConnection remains open:

  • SSHConnection
  • SSHSession
  • SFTPClient

If you call connection.close(), or if a withConnection(...) scope ends, later use fails with SSHClientError.connectionScopeEnded.

That rule applies to child wrappers:

  • SSHSession
  • SFTPClient
  • raw forwarding channel wrappers
import Traversio

func invalidExample() async throws {
    let configuration = SSHClientConfiguration(
        host: "example.com",
        username: "deploy",
        authentication: .password("secret"),
        hostKeyPolicy: .acceptAnyVerifiedHostKey
    )

    var escapedConnection: SSHConnection?

    let connection = try await SSHClient.connect(configuration: configuration)
    escapedConnection = connection
    await connection.close()

    // Throws SSHClientError.connectionScopeEnded
    _ = try await escapedConnection?.execute("true")
}

Using Public-Key Authentication

Traversio loads OpenSSH Ed25519, ECDSA, and RSA private keys directly. For user-imported or migrated keys, privateKeyPEM(...) also accepts OpenSSL-style PKCS#8 Ed25519/RSA/ECDSA, traditional RSA, and traditional EC PEM containers. Traditional RSA PEM may be unencrypted or passphrase-encrypted with supported OpenSSL legacy headers:

import Traversio

func connectWithECDSAKey() async throws {
    let configuration = SSHClientConfiguration(
        host: "example.com",
        username: "deploy",
        authentication: try .privateKeyPEM(
            contentsOfFile: "/Users/me/.ssh/id_ecdsa",
            passphrase: "correct horse battery staple"
        ),
        hostKeyPolicy: .knownHostsFile("/Users/me/.ssh/known_hosts")
    )

    try await SSHClient.withConnection(configuration: configuration) { connection in
        let result = try await connection.execute("whoami")
        print(String(decoding: result.standardOutput, as: UTF8.self))
    }
}

If you already manage the key material in memory yourself, the raw enum cases remain available:

  • .ed25519PrivateKey(rawRepresentation:)
  • .rsaPrivateKey(pkcs1DERRepresentation:)
  • .ecdsaP256PrivateKey(rawRepresentation:)
  • .ecdsaP384PrivateKey(rawRepresentation:)
  • .ecdsaP521PrivateKey(rawRepresentation:)

Supported limits:

  • The OpenSSH helper path supports Ed25519, RSA, and ECDSA keys encrypted with the OpenSSH bcrypt KDF plus AES ciphers such as aes256-ctr and aes256-cbc.
  • SSH agent-backed authentication is available through SSHAgentClient.
  • Keychain-backed credential loading stays in the application layer.

Traversio also generates a new OpenSSH private key file and matching authorized-key line:

import Traversio

func makeKeyMaterial() throws -> SSHOpenSSHKeyPair {
    try SSHOpenSSHKeyPair.generate(
        algorithm: .ed25519,
        comment: "[email protected]",
        encryption: SSHOpenSSHPrivateKeyEncryption(
            passphrase: "correct horse battery staple"
        )
    )
}

For the full workflow, including file permissions, encrypted export options, and using the returned authenticationMethod directly, see Key Generation.

Inspect Connection Metadata

SSHConnection.metadata gives you useful handshake details after a successful connection:

  • endpoint host and port
  • authenticated username
  • Traversio client identification string
  • remote identification string
  • pre-identification banner lines
  • host-key algorithm
  • SHA-256 fingerprint of the verified host key
  • the trust method that accepted the host key

Example:

let metadata = try await SSHClient.withConnection(configuration: configuration) { connection in
    connection.metadata
}

print(metadata.remoteIdentification)
print(metadata.hostKeyFingerprintSHA256)

On this page