How to Open a Unidirectional QUIC Stream with Iroh
Call conn.open_uni().await? on a noq::Connection—obtained via Iroh's QuicClient configured with the /iroh-qad/0 ALPN—to create a send-only stream for one-way data transmission.
Iroh implements its QUIC transport on top of the noq library, supporting both bidirectional and unidirectional stream types. While bidirectional streams allow simultaneous read and write operations, unidirectional streams provide optimized one-way channels where the opener can only send data and the peer receives via accept_uni().
Establish the QUIC Connection
Before opening any stream, you must create a QUIC endpoint and negotiate a connection. According to the Iroh source code in iroh-relay/src/quic.rs, this involves three main components:
QuicClientstruct (lines 60-66): Holds the QUIC endpoint and TLS client configuration.create_connmethod (lines 53-63): Constructs anoq::Connectionto the remote address.- TLS Configuration: Uses
make_dangerous_client_config()with the Iroh-specific ALPN (/iroh-qad/0).
The connection setup is identical whether you intend to use bidirectional or unidirectional streams:
use std::net::SocketAddr;
use iroh_relay::quic::{QuicClient, ALPN_QUIC_ADDR_DISC};
// 1. 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 Iroh's ALPN
let client_cfg = iroh_relay::tls::make_dangerous_client_config();
// 3. Wrap in QuicClient
let quic_client = QuicClient::new(client_ep, client_cfg);
// 4. Connect to remote server
let server_addr: SocketAddr = "127.0.0.1:4433".parse()?;
let conn = quic_client.create_conn(server_addr, "localhost").await?;
Open a Unidirectional Send Stream
Once you have a valid noq::Connection, call open_uni() (as opposed to open_bi() used for bidirectional streams) to obtain a send-only stream. This method returns a stream that implements AsyncWrite but not AsyncRead, enforcing the one-way constraint at the type level.
Following the connection pattern shown in the quic_endpoint_basic test (lines 84-106), unidirectional streams are created as follows:
// Open a unidirectional stream (send-only for the opener)
let mut send_stream = conn.open_uni().await?;
// Write data - unidirectional streams do not support reading from the opener's side
send_stream.write_all(b"Telemetry data").await?;
// Explicitly signal the end of data to the peer
send_stream.finish().await?;
Unlike bidirectional streams where the same handle manages both directions, unidirectional streams require the peer to explicitly accept the incoming stream using accept_uni() on their connection object.
Handle Incoming Unidirectional Streams
On the server side, within the connection handler referenced in handle_connection (lines 26-38 of iroh-relay/src/quic.rs), you accept unidirectional streams using the counterpart method:
// Server-side: accept a unidirectional stream from the client
let mut recv_stream = conn.accept_uni().await?;
// Read the incoming data
let mut buf = vec![0u8; 1024];
let n = recv_stream.read(&mut buf).await?;
println!("Received: {}", String::from_utf8_lossy(&buf[..n]));
The server treats stream closure as a normal shutdown when the client calls finish() or drops the stream, consistent with the graceful shutdown handling shown in the handle_connection implementation.
Key Implementation Files
| File | Purpose |
|---|---|
iroh-relay/src/quic.rs |
Contains QuicClient, connection creation logic, and the quic_endpoint_basic test demonstrating connection establishment. |
iroh-relay/src/quic.rs (lines 26-38) |
handle_connection function showing connection lifecycle management applicable to both stream types. |
iroh-relay/src/tls.rs |
Provides make_dangerous_client_config() for TLS setup required before stream creation. |
Summary
- Unidirectional streams in Iroh are created via
conn.open_uni().await?on anoq::Connectionobtained throughQuicClient::create_conn(). - Connection setup requires proper TLS configuration using the
/iroh-qad/0ALPN, as implemented iniroh-relay/src/quic.rs(lines 53-66). - One-way constraint: The opener gets a write-only stream (
AsyncWrite); the peer must useaccept_uni()to receive a read-only stream (AsyncRead). - Cleanup: Call
finish().await?on the send stream to signal end-of-stream, then close the connection withQUIC_ADDR_DISC_CLOSE_CODEandQUIC_ADDR_DISC_CLOSE_REASON.
Frequently Asked Questions
What is the difference between open_uni() and open_bi() in Iroh?
open_uni() creates a unidirectional stream that allows the opener to only send data, while open_bi() creates a bidirectional stream supporting both send and receive operations on the same handle. Both methods are called on a noq::Connection object obtained through QuicClient::create_conn().
How does the server receive a unidirectional stream?
The server calls accept_uni().await? on the noq::Connection object to accept incoming unidirectional streams. This returns a receive-only stream that implements AsyncRead, allowing the server to consume data sent by the client until the stream is finished.
Can I reuse a unidirectional stream for multiple messages?
Yes, you can call write_all() multiple times on the send stream before calling finish(). However, once finish() is called or the stream is dropped, the stream ID is consumed and cannot be reused. For subsequent communications, you must open a new stream via open_uni().
What ALPN does Iroh use for QUIC connections?
Iroh uses /iroh-qad/0 (QUIC Address Discovery) as the Application-Layer Protocol Negotiation identifier, defined in the ALPN_QUIC_ADDR_DISC constant within the iroh-relay crate. This must be configured in the TLS client and server configs before calling create_conn().
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 →