# Implementing Bi-Directional and Uni-Directional Streams in Iroh

> Implement bi-directional and uni-directional streams in Iroh by wrapping transport protocols. Unlock full-duplex channels or constrained read-only write-only pipes.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-18

---

**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`](https://github.com/n0-computer/iroh/blob/main/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 `AsyncRead` and `AsyncWrite` traits. Typical types include `TcpStream`, `tokio_rustls::client::TlsStream`, and `tokio_websockets::WebSocketStream`.

- **Byte-Framed Layer** converts the raw transport into a `Stream<Item = Result<Bytes, …>>` and a `Sink<Bytes>`. The primary implementation is `WsBytesFramed<T>` for WebSocket connections and `MaybeTlsStream<IO>` for TLS or plain TCP.

- **Proxy Layer** manages optional relay proxying and TLS termination through the `ProxyStream` enum, which variants between `Raw` and `Proxied` states while maintaining `AsyncRead` and `AsyncWrite` implementations.

- **Export-KM Layer** supports TLS-based keying material extraction for noise handshakes via the `ExportKeyingMaterial` trait, implemented for `WsBytesFramed`, `MaybeTlsStream`, and `ProxyStream`.

## 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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.

1. **Transport Selection** establishes the base connection. The client creates a `TcpStream` and optionally upgrades it via `MaybeTlsStream`, which enums between `Raw` and `Tls` variants.

2. **Proxy Handling** inserts relay functionality when direct connectivity is unavailable. The `ProxyStream` type encapsulates either direct connections (`Raw`) or proxied chains (`Proxied`).

3. **WebSocket Framing** applies when communicating through the relay protocol. The `WsBytesFramed` wrapper converts `WebSocketStream` into a byte-oriented interface implementing both `Stream` and `Sink`.

4. **Unified Interface** exposes the final type through the `BytesStreamSink` trait alias. The endpoint logic in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) accepts any `impl 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.

```rust
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.

```rust
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.

```rust
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.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/protos/streams.rs)** implements `WsBytesFramed`, the core bi-directional WebSocket stream wrapper that bridges `WebSocketStream` to byte-oriented `Stream` and `Sink` traits.

- **[`iroh-relay/src/client/streams.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/client/streams.rs)** defines `ProxyStream` and `MaybeTlsStream`, handling raw TCP streams, TLS-wrapped connections, and optional proxy chaining logic.

- **[`iroh-relay/src/server/streams.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server/streams.rs)** provides the server-side counterpart that accepts inbound WebSocket connections and presents them as `WsBytesFramed` instances to the relay server logic.

- **[`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)** contains the higher-level endpoint abstraction that consumes any `impl BytesStreamSink`, agnostic to whether the underlying transport is TCP, TLS, or WebSocket.

- **[`iroh/src/lib.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/lib.rs)** re-exports the public stream types for library users, exposing `WsBytesFramed` and related traits at the crate root.

## Summary

- **Iroh** unifies TCP, TLS, and WebSocket transports behind the `Stream` and `Sink` traits from `n0_future`, enabling protocol-agnostic streaming.
- **Bi-directional streams** use `WsBytesFramed` in [`iroh-relay/src/protos/streams.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/protos/streams.rs) to provide full-duplex communication over WebSocket connections.
- **Uni-directional streams** reuse the same types—`ProxyStream`, `WsBytesFramed`, or `MaybeTlsStream`—but restrict API usage to either `Stream::next()` for reading or `Sink::send()` for writing.
- The **type composition flow** progresses from raw `TcpStream` through `MaybeTlsStream` and `ProxyStream` to the final framed interface, culminating in the `BytesStreamSink` trait alias consumed by [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).
- All transport wrappers implement **keying material extraction** via the `ExportKeyingMaterial` trait 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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.