# How to Open a Bidirectional QUIC Stream with Iroh

> Learn to open a bidirectional QUIC stream with Iroh. Instantiate a QuicClient, create a noq::Connection, and use open_bi() for full-duplex communication.

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

---

**To open a bidirectional QUIC stream with Iroh, instantiate a `QuicClient` with TLS configuration, establish a `noq::Connection` via `create_conn()`, and invoke the `open_bi()` method to obtain a stream implementing both `AsyncRead` and `AsyncWrite` for simultaneous duplex communication.**

Iroh implements its QUIC transport layer on top of the `noq` library, wrapped within the `iroh-relay` crate. According to the n0-computer/iroh source code, bidirectional streams are created by progressing through three distinct phases: endpoint initialization, connection establishment via the `QuicClient` struct in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs), and stream creation using `noq::Connection` methods.

## Understanding the QUIC Architecture

Iroh delegates raw QUIC protocol handling to the **`noq`** crate, which manages connection state, congestion control, and stream multiplexing. In this architecture, a bidirectional stream represents a single logical channel within a QUIC connection where both peers can send and receive data concurrently.

The implementation centers on two primary types:

- **`noq::Connection`**: Represents an established QUIC connection between client and server
- **`QuicClient`**: Wraps the endpoint configuration and TLS state required to initiate connections (defined at lines 60-66 in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs))

## Step-by-Step: Establishing a Bidirectional Stream

### Configure the Endpoint and TLS

Before initiating any connection, you must bind a local UDP socket through `noq::Endpoint` and configure TLS with the Iroh-specific ALPN identifier `/iroh-qad/0`. The source code demonstrates this using `make_dangerous_client_config()` from the `iroh_relay::tls` module for testing environments.

### Create the Connection

Invoke `QuicClient::create_conn()` to perform the QUIC handshake and return a `noq::Connection` object. This method, implemented at lines 53-63 in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs), handles version negotiation and certificate validation, producing a connection ready for stream operations.

### Open the Bidirectional Stream

Call **`open_bi()`** on the `Connection` instance. This async method returns a bidirectional stream suitable for both sending and receiving data. The stream implements standard async I/O traits, allowing you to use `write_all()` to transmit data and `read()` to receive responses from the peer.

## Complete Working Example

The following Rust code demonstrates the full lifecycle from endpoint creation through bidirectional communication, following the pattern validated by the `quic_endpoint_basic` test at lines 84-106 in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs):

```rust
use std::net::SocketAddr;
use iroh_relay::quic::{QuicClient, ALPN_QUIC_ADDR_DISC};
use noq::Connection;               // re‑exported from the `noq` crate

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1️⃣ Create a client endpoint (bind to an OS‑assigned port)
    let client_ep = noq::Endpoint::client("0.0.0.0:0".parse()?)?;
    // 2️⃣ Build a TLS config that trusts the server (dangerous for demo purposes)
    let client_cfg = iroh_relay::tls::make_dangerous_client_config();
    // 3️⃣ Wrap the endpoint + TLS config in an Iroh `QuicClient`
    let quic_client = QuicClient::new(client_ep, client_cfg);

    // 4️⃣ Connect to the remote QUIC server (replace with your server address)
    let server_addr: SocketAddr = "127.0.0.1:4433".parse()?;
    let conn: Connection = quic_client
        .create_conn(server_addr, "localhost")
        .await?;                     // ← `noq::Connection`

    // 5️⃣ Open a **bidirectional** stream
    let mut stream = conn.open_bi().await?;   // `open_bi` → `noq::Connection`

    // 6️⃣ Send data
    stream.write_all(b"Hello, Iroh!").await?;

    // 7️⃣ Receive the echo (or any response)
    let mut buf = vec![0u8; 64];
    let n = stream.read(&mut buf).await?;
    println!("Peer replied: {}", String::from_utf8_lossy(&buf[..n]));

    // 8️⃣ Gracefully close the connection (optional – the server will also close)
    conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
    Ok(())
}

```

## Connection Lifecycle and Error Handling

When the peer terminates the connection gracefully, Iroh receives a `ConnectionError::ApplicationClosed` event. The server-side `handle_connection` function in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) (lines 24-38) demonstrates proper cleanup: closing the endpoint with a predefined error code and awaiting task completion. You should expect this error variant as a normal shutdown signal rather than an exceptional condition.

Bidirectional streams automatically handle flow control and congestion management internally through the `noq` layer. If the remote peer closes their end of the stream while you are still reading, the `read()` method returns `Ok(0)` to indicate EOF, while `write_all()` continues functioning until you close your sending side or the connection terminates.

## Summary

- **Use `QuicClient`** from [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) (lines 60-66) to manage endpoint configuration and TLS state with the Iroh ALPN
- **Call `create_conn()`** (lines 53-63) to establish a `noq::Connection` to the remote peer
- **Invoke `open_bi()`** on the connection to obtain a bidirectional stream supporting simultaneous read/write operations
- **Handle `ApplicationClosed`** as a normal termination signal when the peer shuts down the connection gracefully
- **Reference the `quic_endpoint_basic` test** in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) (lines 84-106) for a validated round-trip implementation

## Frequently Asked Questions

### What is the difference between unidirectional and bidirectional QUIC streams in Iroh?

Unidirectional streams permit data flow in only one direction, created using `open_uni()` on the `noq::Connection`. Bidirectional streams, created via `open_bi()`, provide two independent flows within a single stream—one for each direction—behaving similarly to a TCP socket. Iroh primarily uses bidirectional streams for request-response patterns where both peers need to exchange data simultaneously.

### How does Iroh configure TLS for QUIC connections?

Iroh uses the ALPN protocol identifier `/iroh-qad/0` (referenced as `ALPN_QUIC_ADDR_DISC` in the source) to negotiate application-layer protocols during the TLS handshake. For client connections, the `QuicClient` accepts a TLS configuration created via `make_dangerous_client_config()` for testing or production-grade configurations for deployment. The TLS state is bound to the `noq::Endpoint` during `QuicClient` initialization.

### Why does Iroh use the `noq` crate instead of implementing QUIC directly?

The `noq` crate provides a Rust-native, async-compatible QUIC implementation that handles low-level protocol details like packet encryption, stream multiplexing, and congestion control. By building on `noq`, Iroh leverages battle-tested transport logic while focusing its own development on peer-to-peer networking, hole punching, and relay protocols. This abstraction is visible in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) where `noq::Connection` objects are the primary interface for stream operations.

### How do I gracefully close a bidirectional stream in Iroh?

Call `conn.close(error_code, reason)` on the `noq::Connection` object to initiate a graceful shutdown, as implemented in the `handle_connection` function at lines 24-38 of [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs). This sends a CONNECTION_CLOSE frame to the peer with the specified application error code. For individual streams, dropping the stream object or finishing the write side signals EOF to the remote peer while keeping the connection alive for other streams.