Implementing Bi-Directional and Uni-Directional Streams in Iroh
Iroh implements bi-directional and uni-directional streams by wrapping transport protocols in types that implement both Stream and Sink traits from n0_future, allowing the same underlying connection to function as full-duplex channels or constrained read-only/write-only pipes.
The n0-computer/iroh repository provides a flexible networking stack that abstracts TCP, TLS, QUIC, and WebSocket transports behind unified streaming interfaces. Whether you need full-duplex communication between peers or constrained uni-directional data flow, implementing bi-directional and uni-directional streams in iroh relies on trait-based architecture and the WsBytesFramed and ProxyStream types defined in iroh-relay/src/protos/streams.rs.
Architecture Overview
Iroh organizes network streaming into four distinct layers, each serving a specific purpose in the transport stack.
-
Transport Layer handles raw I/O using Tokio's
AsyncReadandAsyncWritetraits. Typical types includeTcpStream,tokio_rustls::client::TlsStream, andtokio_websockets::WebSocketStream. -
Byte-Framed Layer converts the raw transport into a
Stream<Item = Result<Bytes, …>>and aSink<Bytes>. The primary implementation isWsBytesFramed<T>for WebSocket connections andMaybeTlsStream<IO>for TLS or plain TCP. -
Proxy Layer manages optional relay proxying and TLS termination through the
ProxyStreamenum, which variants betweenRawandProxiedstates while maintainingAsyncReadandAsyncWriteimplementations. -
Export-KM Layer supports TLS-based keying material extraction for noise handshakes via the
ExportKeyingMaterialtrait, implemented forWsBytesFramed,MaybeTlsStream, andProxyStream.
Bi-Directional Stream Implementation
A bi-directional stream in iroh provides full-duplex communication where reading and writing occur simultaneously over the same underlying connection. This is achieved through the WsBytesFramed<T> struct in iroh-relay/src/protos/streams.rs, which implements both the Stream trait for inbound bytes and the Sink<Bytes> trait for outbound bytes.
The implementation filters WebSocket control frames (ping, pong, close) while yielding binary payloads through the Stream interface. Because the same tokio_websockets::WebSocketStream object handles both directions, callers can interleave calls to Stream::next() and Sink::start_send() without blocking.
Higher-level components consume any type implementing BytesStreamSink, a trait alias combining Stream<Item = Result<Bytes, …>> and Sink<Bytes>. This abstraction allows WsBytesFramed and MaybeTlsStream to be used interchangeably in the connection logic located in iroh/src/endpoint.rs.
Uni-Directional Stream Patterns
Uni-directional usage leverages the same underlying types but restricts the API surface to either read-only or write-only operations.
Read-only streams treat the type strictly as a Stream. You poll Stream::next() to receive Bytes payloads while never invoking Sink::start_send. This pattern appears when receiving files or server-sent events where no client response is required.
Write-only streams utilize only the Sink implementation. You call Sink::send() or Sink::start_send() to transmit data without ever polling the stream side. This suits telemetry uploads or command issuance scenarios where the peer expects no response payload.
Both patterns use identical type definitions—ProxyStream, WsBytesFramed, or MaybeTlsStream—differing only in which trait methods the caller invokes.
Type Composition Flow
Implementing streams in iroh follows a four-stage composition process that progressively wraps raw transports into application-ready abstractions.
-
Transport Selection establishes the base connection. The client creates a
TcpStreamand optionally upgrades it viaMaybeTlsStream, which enums betweenRawandTlsvariants. -
Proxy Handling inserts relay functionality when direct connectivity is unavailable. The
ProxyStreamtype encapsulates either direct connections (Raw) or proxied chains (Proxied). -
WebSocket Framing applies when communicating through the relay protocol. The
WsBytesFramedwrapper convertsWebSocketStreaminto a byte-oriented interface implementing bothStreamandSink. -
Unified Interface exposes the final type through the
BytesStreamSinktrait alias. The endpoint logic iniroh/src/endpoint.rsaccepts anyimpl BytesStreamSink, enabling polymorphic usage across transport types.
Practical Implementation Examples
Creating a Bi-Directional WebSocket Stream
The following example demonstrates establishing a full-duplex WebSocket connection using WsBytesFramed to wrap the raw WebSocket transport.
use iroh_relay::protos::streams::WsBytesFramed;
use tokio_websockets::WebSocketStream;
use tokio::net::TcpStream;
use bytes::Bytes;
use n0_future::{Stream, Sink};
async fn bi_directional_ws(url: &str) -> Result<(), Box<dyn std::error::Error>> {
// 1️⃣ Connect a plain TCP socket (or TLS‑wrapped socket)
let tcp = TcpStream::connect("example.com:443").await?;
// 2️⃣ Upgrade to a WebSocket
let ws_stream = WebSocketStream::new(tcp);
// 3️⃣ Wrap in WsBytesFramed which implements Stream + Sink
let mut ws = WsBytesFramed { io: ws_stream };
// Send a message (write side)
ws.start_send(Bytes::from_static(b"hello iroh")).await?;
// Receive a message (read side)
if let Some(Ok(payload)) = ws.next().await {
println!("received: {}", std::str::from_utf8(&payload)?);
}
Ok(())
}
The WsBytesFramed type implements both Stream and Sink, enabling full-duplex communication over the same underlying socket.
Read-Only Stream Usage
This pattern consumes only the Stream implementation, ignoring the Sink capabilities of the underlying type.
use iroh_relay::client::streams::ProxyStream;
use futures::StreamExt; // for `.next()`
async fn read_only(proxy: ProxyStream) -> Result<(), Box<dyn std::error::Error>> {
let mut read = proxy; // we only use the read side
while let Some(Ok(chunk)) = read.next().await {
// Process inbound data
println!("got {} bytes", chunk.len());
}
Ok(())
}
Only the Stream part is exercised; the Sink methods are never called, creating a uni-directional read pipe.
Write-Only Stream Usage
Conversely, this example demonstrates uni-directional writing by utilizing only the Sink trait.
use iroh_relay::client::streams::ProxyStream;
use bytes::Bytes;
use n0_future::SinkExt; // for `.send()`
async fn write_only(mut proxy: ProxyStream) -> Result<(), Box<dyn std::error::Error>> {
let data = Bytes::from_static(b"file contents");
proxy.send(data).await?;
Ok(())
}
Only the Sink trait is used; no reads are performed, establishing a uni-directional write channel.
Key Source Files
The stream implementations are distributed across several critical files in the n0-computer/iroh repository.
-
iroh-relay/src/protos/streams.rsimplementsWsBytesFramed, the core bi-directional WebSocket stream wrapper that bridgesWebSocketStreamto byte-orientedStreamandSinktraits. -
iroh-relay/src/client/streams.rsdefinesProxyStreamandMaybeTlsStream, handling raw TCP streams, TLS-wrapped connections, and optional proxy chaining logic. -
iroh-relay/src/server/streams.rsprovides the server-side counterpart that accepts inbound WebSocket connections and presents them asWsBytesFramedinstances to the relay server logic. -
iroh/src/endpoint.rscontains the higher-level endpoint abstraction that consumes anyimpl BytesStreamSink, agnostic to whether the underlying transport is TCP, TLS, or WebSocket. -
iroh/src/lib.rsre-exports the public stream types for library users, exposingWsBytesFramedand related traits at the crate root.
Summary
- Iroh unifies TCP, TLS, and WebSocket transports behind the
StreamandSinktraits fromn0_future, enabling protocol-agnostic streaming. - Bi-directional streams use
WsBytesFramediniroh-relay/src/protos/streams.rsto provide full-duplex communication over WebSocket connections. - Uni-directional streams reuse the same types—
ProxyStream,WsBytesFramed, orMaybeTlsStream—but restrict API usage to eitherStream::next()for reading orSink::send()for writing. - The type composition flow progresses from raw
TcpStreamthroughMaybeTlsStreamandProxyStreamto the final framed interface, culminating in theBytesStreamSinktrait alias consumed byiroh/src/endpoint.rs. - All transport wrappers implement keying material extraction via the
ExportKeyingMaterialtrait to support cryptographic handshakes.
Frequently Asked Questions
How does iroh handle bi-directional communication over WebSockets?
Iroh handles bi-directional WebSocket communication through the WsBytesFramed type in iroh-relay/src/protos/streams.rs. This struct wraps tokio_websockets::WebSocketStream and implements both Stream<Item = Result<Bytes, …>> for reading binary payloads and Sink<Bytes> for writing them. The implementation filters out WebSocket control frames like ping and close, exposing only the byte payload to callers while maintaining full-duplex capability.
Can I use the same stream type for both reading and writing in iroh?
Yes, the same concrete types—WsBytesFramed, ProxyStream, or MaybeTlsStream—serve both reading and writing purposes. These types implement the BytesStreamSink trait alias, which combines Stream and Sink capabilities. For bi-directional usage, you interleave calls to Stream::next() and Sink::start_send(). For uni-directional usage, you simply ignore the methods corresponding to the direction you do not need.
What is the difference between WsBytesFramed and ProxyStream?
WsBytesFramed is a protocol-specific wrapper that converts WebSocket message frames into byte streams, implementing Stream and Sink for binary payloads. ProxyStream is a higher-level abstraction in iroh-relay/src/client/streams.rs that represents either a direct connection (Raw) or a relay-proxied connection (Proxied). ProxyStream may internally contain a WsBytesFramed when communicating through the relay protocol, but it also handles raw TCP and TLS streams without WebSocket framing.
How do I implement a read-only stream in iroh?
To implement a read-only stream, obtain any type implementing Stream—such as ProxyStream or WsBytesFramed—and poll it using Stream::next() or the next() method from futures::StreamExt. Do not call any Sink methods such as send() or start_send(). The type system allows this because Stream and Sink are separate traits, and consuming only the Stream interface creates a uni-directional read pipe without affecting the underlying connection state.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →