Files
alknet/docs/architecture/decisions/003-iroh-stream-join.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

1.9 KiB

ADR-003: iroh Stream via tokio::io::join

Status

Accepted

Context

iroh's QUIC implementation provides separate RecvStream (implements AsyncRead) and SendStream (implements AsyncWrite) for each bidirectional channel opened via open_bi() / accept_bi(). russh's connect_stream() and run_stream() require a single type implementing both AsyncRead and AsyncWrite.

Options considered:

  1. tokio::io::join(recv, send) — Combines the two halves into Join<RecvStream, SendStream> which implements both traits.
  2. Custom IrohStream wrapper — A struct with recv and send fields that delegates AsyncRead to recv and AsyncWrite to send.
  3. Using iroh's Connection directly — Opening a new open_bi() for each SSH channel instead of running SSH over a single stream.

Decision

Use tokio::io::join(recv_stream, send_stream) (Option 1).

One line of code, correct trait implementations, no custom types needed. The Join<A, B> type implements AsyncRead using A and AsyncWrite using B, which maps directly to iroh's split stream model.

If profiling later shows overhead (unlikely — it's just method dispatch), we can switch to a custom wrapper. But YAGNI until demonstrated.

Option 3 was rejected because it would require modifying russh to understand iroh connections. The whole point of the transport trait is that SSH doesn't know about iroh.

Consequences

  • Positive: Minimal code. One line to bridge iroh and russh.
  • Positive: No custom types to maintain.
  • Positive: Correct AsyncRead + AsyncWrite behavior — Poll::Pending on one half doesn't affect the other.
  • Negative: None identified. The Join type is a standard tokio combinator with well-tested semantics.

References