# How to Open a Bi-Directional QUIC Stream in Iroh: A Complete Guide

> Learn to open a bi-directional QUIC stream in Iroh with this complete guide. Quickly establish connections using the open bi method after configuring your QUIC endpoint.

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

---

**To open a bi-directional QUIC stream in Iroh, call `open_bi()` on a `noq::Connection` obtained via `QuicClient::create_conn()` after configuring a QUIC endpoint with the Iroh-specific ALPN.**

Iroh provides a robust networking stack built on the **`noq`** QUIC library, enabling fast and secure peer-to-peer communication. Opening a bi-directional QUIC stream allows you to send and receive data simultaneously over a single connection, similar to a TCP socket but with the performance benefits of QUIC. This guide demonstrates the exact implementation details found in the `n0-computer/iroh` repository.

## Prerequisites: Understanding Iroh's QUIC Architecture

Iroh's QUIC transport layer relies on the `noq` library. Before opening a stream, you must establish a `noq::Connection` by configuring a QUIC endpoint with Iroh's specific **ALPN** (`/iroh-qad/0`) and TLS settings. The core logic resides in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs), which manages the transition from raw QUIC connections to usable Iroh transport objects.

## Step-by-Step: Opening a Bi-Directional QUIC Stream

### 1. Create a QUIC Endpoint

First, instantiate a QUIC endpoint that binds to a local address. For clients, binding to `0.0.0.0:0` lets the OS assign an available port.

```rust
use std::net::SocketAddr;

// Create a client endpoint bound to an OS-assigned port
let client_ep = noq::Endpoint::client("0.0.0.0:0".parse()?)?;

```

### 2. Configure TLS with the Iroh ALPN

Iroh requires TLS configuration with the `ALPN_QUIC_ADDR_DISC` protocol identifier. In [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs), the `QuicClient` struct (defined at lines 60-66) encapsulates the endpoint and client configuration.

```rust
use iroh_relay::quic::{QuicClient, ALPN_QUIC_ADDR_DISC};
use iroh_relay::tls::make_dangerous_client_config; // For testing only

// Build TLS configuration
let client_cfg = make_dangerous_client_config();

// Wrap the endpoint and config in Iroh's QuicClient
let quic_client = QuicClient::new(client_ep, client_cfg);

```

### 3. Establish the Connection

Use `QuicClient::create_conn()` (implemented at lines 53-63 in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs)) to connect to a remote server. This returns a `noq::Connection` ready for stream operations.

```rust
let server_addr: SocketAddr = "127.0.0.1:4433".parse()?;
let conn = quic_client
    .create_conn(server_addr, "localhost")
    .await?; // Returns noq::Connection

```

### 4. Open the Bi-Directional Stream

Once you have the connection, call `open_bi()` to create a bidirectional stream. This returns a stream that implements `AsyncRead` and `AsyncWrite`.

```rust
// Open a bi-directional stream
let mut stream = conn.open_bi().await?;

```

### 5. Read and Write Data

The same stream handle supports both sending and receiving. Write data using `write_all()` and read responses with `read()`.

```rust
// Send data to the peer
stream.write_all(b"Hello, Iroh!").await?;

// Receive the peer's response
let mut buf = vec![0u8; 64];
let n = stream.read(&mut buf).await?;
println!("Peer replied: {}", String::from_utf8_lossy(&buf[..n]));

```

## Server-Side Connection Handling

On the server side, Iroh handles incoming connections through the `handle_connection` function (lines 26-38 in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs)). This function processes the connection until the peer closes it, treating `ConnectionError::ApplicationClosed` as a normal shutdown sequence.

## Complete Implementation Example

The test `quic_endpoint_basic` in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) (lines 84-106) demonstrates the full lifecycle. Here is a complete client example based on that implementation:

```rust
use std::net::SocketAddr;
use iroh_relay::quic::{QuicClient, ALPN_QUIC_ADDR_DISC};
use noq::Connection;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Create a client endpoint
    let client_ep = noq::Endpoint::client("0.0.0.0:0".parse()?)?;
    
    // 2. Configure TLS (using test-only helper for demo purposes)
    let client_cfg = iroh_relay::tls::make_dangerous_client_config();
    let quic_client = QuicClient::new(client_ep, client_cfg);

    // 3. Connect to remote server
    let server_addr: SocketAddr = "127.0.0.1:4433".parse()?;
    let conn: Connection = quic_client
        .create_conn(server_addr, "localhost")
        .await?;

    // 4. Open a bi-directional stream
    let mut stream = conn.open_bi().await?;

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

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

    // 7. Gracefully close the connection
    conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
    Ok(())
}

```

## Summary

- Iroh uses the `noq` library for QUIC transport, exposed through `noq::Connection`.
- The `QuicClient::create_conn()` method in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) establishes the underlying connection.
- Call `open_bi()` on the connection to obtain a stream implementing `AsyncRead` and `AsyncWrite`.
- Configure TLS with the Iroh-specific ALPN (`/iroh-qad/0`) before connecting.
- The `handle_connection` function in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) manages server-side lifecycle and graceful shutdowns.

## Frequently Asked Questions

### What is the difference between `open_bi()` and `open_uni()` in Iroh?

`open_bi()` creates a bi-directional stream allowing both endpoints to send and receive data simultaneously, similar to a TCP connection. `open_uni()` creates a uni-directional stream where only the initiating side can send data, and the receiving side can only read.

### How do I configure TLS for Iroh QUIC connections?

According to [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs), TLS configuration is handled when constructing the `QuicClient`. For production, use valid certificates; for testing, `iroh_relay::tls::make_dangerous_client_config()` creates a minimal configuration that trusts all servers (insecure and only for development).

### Where does Iroh handle graceful connection shutdown?

The `handle_connection` function in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) (lines 26-38) manages connection closure. It treats `ConnectionError::ApplicationClosed` as a normal shutdown, allowing the endpoint to close with a specific error code (`QUIC_ADDR_DISC_CLOSE_CODE`) and reason string.

### What async traits does the bi-directional stream implement?

The stream returned by `open_bi()` implements `AsyncRead` and `AsyncWrite` from the Tokio async ecosystem, enabling standard async IO operations like `write_all()`, `read()`, and `read_exact()`.