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

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, 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.

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, the QuicClient struct (defined at lines 60-66) encapsulates the endpoint and client configuration.

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) to connect to a remote server. This returns a noq::Connection ready for stream operations.

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.

// 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().

// 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). 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 (lines 84-106) demonstrates the full lifecycle. Here is a complete client example based on that implementation:

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 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 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, 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 (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().

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →