Traversio

Quickstart

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

Before You Start

The public transport path uses the new Apple Network framework APIs available on platform release 26 and later. In practice that means:

  • Traversio targets Apple platforms first.
  • SSHClient.connect(configuration:) and SSHClient.withConnection(configuration:_:) are only available on macOS, iOS, tvOS, watchOS, and visionOS 26 or later.
  • The package uses swift-tools-version: 6.2.

Traversio is suitable for evaluation and staged integration. Production hardening and wider interoperability work continue in parallel with the public API.

Read Mental Model first if you want the object graph before reading code examples.

Add the Package

Until the project starts shipping tagged releases, the safest way to consume the current docs examples is to point SwiftPM at main:

// Package.swift
dependencies: [
    .package(
        url: "https://github.com/GitSwiftLLC/Traversio.git",
        branch: "main"
    )
]

Then add the library to your target:

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

First 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
import Traversio

@available(macOS 26.0, iOS 26.0, *)
func runRemoteUname() async throws -> String {
    let configuration = SSHClientConfiguration(
        host: "example.com",
        username: "deploy",
        authentication: .password("correct horse battery staple"),
        hostKeyPolicy: .knownHostsFile("/Users/me/.ssh/known_hosts")
    )

    let connection = try await SSHClient.connect(configuration: configuration)
    let result = try await connection.execute("uname -a")
    await connection.close()

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

Use withConnection(...) when closure-scoped ownership fits your flow. It performs the same setup and cleanup automatically.

Connection Flow

Within one connection call, Traversio performs:

  1. TCP connection setup through the Apple 26+ transport adapter.
  2. SSH identification exchange.
  3. Curve25519 or ecdh-sha2-nistp256 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, and leaves keepalive and explicit timeouts disabled.

If you want RFC 4253 zlib, delayed OpenSSH compression, tighter client-initiated rekey thresholds, post-auth idle keepalive, or bounded waits for setup/reply paths, set them explicitly on SSHClientConfiguration:

import Traversio

@available(macOS 26.0, iOS 26.0, *)
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,
            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
  • explicit timeouts for connection setup and prompt reply waits

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 OSLog
import Traversio

@available(macOS 26.0, iOS 26.0, *)
func runLoggedCommand(configuration: SSHClientConfiguration) async throws {
    let logHandler = SSHClientLogHandler.osLog(
        subsystem: "com.example.monitor",
        category: "ssh",
        minimumLevel: .info
    )

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

If you want a bounded recent-event buffer for support export, attach SSHClientLogRecorder through diagnostics.logHandler(minimumLevel:) or SSHClientLogHandler.recorder(...).

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

@available(macOS 26.0, iOS 26.0, *)
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:

import Traversio

@available(macOS 26.0, iOS 26.0, *)
func connectWithECDSAKey() async throws {
    let configuration = SSHClientConfiguration(
        host: "example.com",
        username: "deploy",
        authentication: try .ecdsaPrivateKey(
            contentsOfOpenSSHPrivateKeyFile: "/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.
  • Agent-backed authentication and keychain-backed credential loading remain outside the current public surface.

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

import Traversio

@available(macOS 26.0, iOS 26.0, *)
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