Files
wraith/docs/architecture/decisions/001-pluggable-transport.md
glm-5.1 dad8224686 Add architecture specification for wraith SSH tunnel tool
Docs:
- README.md: index with doc table, ADR table, lifecycle definitions
- overview.md: purpose, exports, dependencies, constraints
- transport.md: Transport trait, TCP/TLS/iroh implementations, stream join
- client.md: SOCKS5 server, port forwarding, channel manager, reconnection
- server.md: auth, channel handling, stealth mode, outbound proxy
- tun-shim.md: separate privileged process, virtual DNS, --unshare mode
- napi-and-pubsub.md: NAPI wrapper, pubsub event target adapter

ADRs:
- 001: Pluggable transport via AsyncRead+AsyncWrite trait
- 002: TUN shim as separate process
- 003: iroh stream via tokio::io::join
- 004: SSH runs over transport, not alongside
- 005: SOCKS5 as primary interface, TUN as add-on
- 006(007): NAPI exposes single duplex stream

Open questions: 11 items covering TLS certs, iroh relay defaults,
Windows TUN, auth expansion, NAPI surface, TCP reconstruction
2026-06-01 15:01:45 +00:00

2.1 KiB

ADR-001: Pluggable Transport via AsyncRead+AsyncWrite Trait

Status

Accepted

Context

Wraith needs to support multiple transport modes (TCP, TLS, iroh) for SSH sessions. Each mode has different connection establishment logic but produces the same result: a bidirectional byte stream. Without an abstraction, each transport would need its own SSH connection code path.

russh's client::connect_stream() and server::run_stream() both accept AsyncRead + AsyncWrite + Unpin + Send, meaning SSH is already transport-agnostic at the API level. The design question is whether to enshrine this in wraith's own type system or handle each transport case-by-case.

Decision

Define a Transport trait that produces AsyncRead + AsyncWrite + Unpin + Send streams. Each transport (TCP, TLS, iroh) implements this trait. The SSH layer calls transport.connect() and passes the result to russh::client::connect_stream().

On the server side, define a TransportAcceptor trait that produces incoming streams. Each acceptor (TCP listener, TLS listener, iroh endpoint) implements this trait. The server calls acceptor.accept() and passes the result to russh::server::run_stream().

This makes adding a new transport (e.g., WebSocket, QUIC directly) a matter of implementing the trait, not modifying SSH code.

Consequences

  • Positive: Clean separation between transport and protocol. Adding transports is additive. SSH code is transport-agnostic.
  • Positive: Testing is simplified — mock transports can produce in-memory streams.
  • Negative: Slight indirection for the single-transport case (just TCP). The trait boilerplate is minimal though.
  • Negative: The trait must be object-safe if we want dynamic dispatch. Using impl Trait in function signatures avoids this but limits runtime transport selection. CLI-selected transport needs dynamic dispatch: Box<dyn Transport<Stream = Box<dyn AsyncRead+AsyncWrite+Unpin+Send>>>.

References