swift-et

July 21, 2026 · View on GitHub

GitHub Twitter Email Discord Support me

Pure Swift Eternal Terminal (ET) protocol client for Apple platforms.

This library implements the client side of the Eternal Terminal wire protocol (protocol version 6): bootstrap command generation/parsing, encrypted transport, reconnect/resume, terminal I/O, and port forwarding. The server, an SSH implementation, and terminal UI/rendering are intentionally out of scope so consumers can integrate their preferred SSH stack and renderer.

Features

  • Wire-compatible with the C++ etserver (verified end-to-end against Eternal Terminal 7.0.0)
  • Pure Swift XSalsa20-Poly1305, byte-compatible with libsodium crypto_secretbox — no runtime crypto dependency
  • Seamless reconnection and process relaunch recovery using sequence-numbered ciphertext checkpoints
  • Terminal session API: async/await, AsyncStream output, keystroke input, window resize
  • SSH bootstrap adapter with bring-your-own-SSH execution
  • Forward and reverse port tunnels, port ranges, Unix sockets, jumphost support
  • Split modules:
    • ETCrypto — pure Swift XSalsa20-Poly1305 and nonce management
    • ETCore — protobuf messages, packets, framing, and reliable catchup
    • ETTransport — internal transport abstraction and Network.framework implementation
    • ETBootstrap — injected etterminal launch command and credential parser
    • ETSession — connection lifecycle, terminal API, forwarding, and jumphost support
  • Real local etserver E2E test harness
  • Release-mode benchmarks with a libsodium baseline

Platforms

  • iOS 16+
  • macOS 13+
  • Swift tools 6.0+

Installation

Add to Package.swift:

dependencies: [
    .package(url: "https://github.com/wiedymi/swift-et", from: "0.1.5")
]

Then add ETSession and ETBootstrap to your app target dependencies:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "ETSession", package: "swift-et"),
        .product(name: "ETBootstrap", package: "swift-et")
    ]
)

Usage

Apps normally import the session and bootstrap surfaces:

import ETBootstrap
import ETSession

You can supply credentials acquired out of band:

let session = try ETTerminalSession(
    host: "example.com",
    port: 2022,
    clientID: clientID,
    passkey: passkey
)

try await session.connect()

Task {
    for await data in session.output {
        renderer.feed(data)
    }
}

try await session.send(Data("ls -la\n".utf8))
try await session.resize(
    rows: 40,
    cols: 120,
    pixelWidth: 1440,
    pixelHeight: 900
)

Bootstrap

ETBootstrap mirrors the C++ client's etterminal command and IDPASSKEY: parser. The library never opens SSH itself; inject your existing SSH client:

struct AppSSHExecutor: ETBootstrapExecutor {
    let ssh: MySSHClient

    func run(command: String) async throws -> String {
        try await ssh.run(command, captureOutput: true)
    }
}

let session = ETTerminalSession(
    host: "example.com",
    port: 2022,
    bootstrapExecutor: AppSSHExecutor(ssh: ssh),
    bootstrapOptions: ETBootstrapOptions(
        term: "xterm-256color",
        serverFifo: "/tmp/etserver.fifo"
    )
)

try await session.connect()

Port forwarding:

let session = try ETTerminalSession(
    host: "example.com",
    port: 2022,
    clientID: clientID,
    passkey: passkey,
    tunnelSpecification: "8080:8080",
    reverseTunnelSpecification: "9090:9090"
)

The full lifecycle (bootstrapping, connecting, connected, disconnected, reconnecting, and terminal states) is observable via session.stateChanges. If your network monitor reports a meaningful path change, nudge recovery without waiting for the normal retry:

await session.notifyNetworkPathChanged()

To resume the same server-side session after an app relaunch, persist ETSessionCheckpoint separately from the passkey, then restore both:

let checkpoint = try await session.prepareForApplicationBackground()
// Persist checkpoint; keep the passkey in Keychain or another credential store.

let restored = try ETTerminalSession(
    host: "example.com",
    clientID: clientID,
    passkey: passkey,
    checkpoint: checkpoint
)
try await restored.connect()

If the process remains alive, call resumeFromApplicationBackground() after foregrounding.

Testing

swift test

End-to-end tests against a real etserver (brew install MisterTea/et/et):

ET_INTEGRATION=1 swift test --filter ETIntegrationTests

Benchmarks:

swift run -c release Benchmarks

License

MIT