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
noqlibrary for QUIC transport, exposed throughnoq::Connection. - The
QuicClient::create_conn()method iniroh-relay/src/quic.rsestablishes the underlying connection. - Call
open_bi()on the connection to obtain a stream implementingAsyncReadandAsyncWrite. - Configure TLS with the Iroh-specific ALPN (
/iroh-qad/0) before connecting. - The
handle_connectionfunction iniroh-relay/src/quic.rsmanages 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →