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
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:
tokio::io::join(recv, send)— Combines the two halves intoJoin<RecvStream, SendStream>which implements both traits.- Custom
IrohStreamwrapper — A struct withrecvandsendfields that delegatesAsyncReadtorecvandAsyncWritetosend. - Using iroh's
Connectiondirectly — Opening a newopen_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+AsyncWritebehavior —Poll::Pendingon one half doesn't affect the other. - Negative: None identified. The
Jointype is a standard tokio combinator with well-tested semantics.