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.
- 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,
AsyncStreamoutput, 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 managementETCore— protobuf messages, packets, framing, and reliable catchupETTransport— internal transport abstraction and Network.framework implementationETBootstrap— injectedetterminallaunch command and credential parserETSession— connection lifecycle, terminal API, forwarding, and jumphost support
- Real local
etserverE2E test harness - Release-mode benchmarks with a libsodium baseline
- iOS 16+
- macOS 13+
- Swift tools 6.0+
Add to Package.swift:
dependencies: [
.package(url: "https://github.com/wiedymi/swift-et", from: "0.1.6")
]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")
]
)Apps normally import the session and bootstrap surfaces:
import ETBootstrap
import ETSessionYou 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
)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.
Recovery rejection for an existing session emits ETConnectionState.sessionEnded, matching the official ET client. Initial key rejection remains an error. Session end does not distinguish shell exit from server-side session loss.
swift testEnd-to-end tests against a real etserver (brew install MisterTea/et/et):
ET_INTEGRATION=1 swift test --filter ETIntegrationTestsBenchmarks:
swift run -c release BenchmarksMIT