How iroh's 0-RTT Connection Works: Zero-Round-Trip QUIC Establishment
iroh enables zero-round-trip-time (0-RTT) connections by allowing clients to resume cached TLS sessions through the into_0rtt() method on a Connecting object, which returns an OutgoingZeroRttConnection that permits immediate data transmission before the QUIC handshake completes.
iroh implements 0-RTT connection establishment using the QUIC stack provided by the noq crate. This mechanism eliminates the initial handshake latency for returning clients by leveraging TLS session resumption, allowing application data to flow immediately while the cryptographic handshake finishes in the background.
The QUIC Foundation for 0-RTT
At the transport layer, iroh relies on the noq crate to handle the underlying QUIC protocol implementation. When a client has previously communicated with a server, it can store the TLS session parameters and attempt to resume them on subsequent connections. This resumption capability is the foundation of 0-RTT: the client sends data alongside the handshake rather than waiting for the server to confirm the connection.
The 0-RTT Connection Flow
The establishment process follows a specific sequence that attempts to convert a standard connection into a 0-RTT variant, then handles the server's acceptance or rejection of the early data.
Creating the Client Endpoint
The process begins with initializing an iroh endpoint that wraps the underlying QUIC transport. You create a client endpoint by binding to a local address and specifying the remote peer's credentials.
let client = iroh::Endpoint::client("0.0.0.0:0".parse::<SocketAddr>()?).await?;
let connecting = client.connect(remote_addr, remote_name);
Converting to 0-RTT Mode
Once you have a Connecting object, you attempt to convert it to 0-RTT mode using the into_0rtt() method. According to the source code in iroh/src/endpoint/connection.rs (lines 92–102), this method internally calls noq::Connecting::into_0rtt() to request TLS session resumption.
The method returns a Result<OutgoingZeroRttConnection, Connecting>:
- Success: Returns
OutgoingZeroRttConnectionif the session can be resumed - Failure: Returns the original
Connectingif 0-RTT is not possible
let outgoing = match connecting.into_0rtt() {
Ok(zrt) => zrt, // 0-RTT possible, early data allowed
Err(conn) => conn, // Fall back to normal handshake
};
Handling Acceptance or Rejection
The OutgoingZeroRttConnection contains a future that resolves when the handshake completes and indicates whether the server accepted the 0-RTT data. As implemented in iroh/src/endpoint/connection.rs (lines 131–148), the internal zrtt_accepted future resolves to a boolean indicating server acceptance.
You must await handshake_completed() (lines 240–260 in the same file), which returns a ZeroRttStatus enum:
ZeroRttStatus::Accepted(conn): The server accepted early data; streams opened before the handshake remain validZeroRttStatus::Rejected(conn): The server rejected early data; pre-handshake streams will error and must be reopened
match outgoing.handshake_completed().await? {
iroh::endpoint::ZeroRttStatus::Accepted(conn) => {
// Early data succeeded, use existing streams
let mut stream = conn.open_bi().await?;
stream.send_data(b"early data".into()).await?;
}
iroh::endpoint::ZeroRttStatus::Rejected(conn) => {
// Must resend data after handshake completes
}
}
Complete Implementation Example
The following example demonstrates the full client-side flow, including binding, connecting, attempting 0-RTT conversion, and handling the result. This pattern mirrors the runnable demonstration found in iroh/examples/0rtt.rs.
use iroh::{Endpoint, Result};
use std::net::SocketAddr;
#[tokio::main]
async fn main() -> Result<()> {
// Create a client endpoint (bind to an OS-chosen UDP port)
let client = Endpoint::client("0.0.0.0:0".parse::<SocketAddr>()?).await?;
// Initiate a connection to a peer
let connecting = client.connect("198.51.100.42:443".parse()?, "example.com");
// Attempt 0-RTT conversion
let outgoing = match connecting.into_0rtt() {
Ok(zrt) => zrt,
Err(conn) => conn,
};
// Wait for handshake completion and check status
match outgoing.handshake_completed().await? {
iroh::endpoint::ZeroRttStatus::Accepted(conn) => {
println!("0-RTT accepted! Streams opened earlier are usable.");
let mut stream = conn.open_bi().await?;
stream.send_data(b"early data".into()).await?;
}
iroh::endpoint::ZeroRttStatus::Rejected(conn) => {
println!("0-RTT rejected – resend data after handshake.");
}
}
Ok(())
}
Key Source Files
The implementation spans several critical files in the iroh repository:
iroh/src/endpoint/connection.rs: Contains theConnecting::into_0rtt()method (lines 92–102), theZeroRttStatusenum definition, and thehandshake_completed()future logic (lines 129–148 and 240–260)iroh/examples/0rtt.rs: Provides a minimal runnable example demonstrating the complete 0-RTT client flowiroh/src/endpoint/quic.rs: Configures QUIC transport parameters that affect connection establishment timing
Summary
- iroh uses the noq crate to provide QUIC transport capabilities including TLS session resumption
Connecting::into_0rtt()iniroh/src/endpoint/connection.rsattempts to resume sessions and enable immediate data transmission- The conversion returns
OutgoingZeroRttConnectionwhen successful, or falls back to standardConnectingif resumption fails handshake_completed()resolves toZeroRttStatus::Accepted(early data valid) orRejected(must resend after handshake)- Application code must handle rejection by retrying stream operations after the handshake finishes
Frequently Asked Questions
What happens if the server rejects 0-RTT data?
If the server rejects the 0-RTT data, the handshake_completed() future resolves to ZeroRttStatus::Rejected(conn). Any streams opened before the handshake completed will error, and the application must reopen them after the handshake finishes to ensure data delivery. The connection itself remains valid for use after the rejection.
How does iroh store TLS session tickets for 0-RTT?
The iroh source code delegates TLS session management to the underlying noq QUIC implementation. The into_0rtt() method in iroh/src/endpoint/connection.rs simply requests resumption via noq::Connecting::into_0rtt(), which handles the cryptographic state caching required for session resumption according to QUIC-TLS specifications.
Is 0-RTT secure for all types of data?
0-RTT data is encrypted and integrity-protected, but it lacks forward secrecy guarantees because it is sent before the complete handshake exchange. According to the iroh architecture, applications should treat 0-RTT data as potentially replayable and avoid sending sensitive idempotent operations or non-idempotent state-changing requests until after receiving ZeroRttStatus::Accepted.
Where can I find a complete working example?
The repository includes a dedicated example at iroh/examples/0rtt.rs that demonstrates the complete client workflow from endpoint creation through handshake completion. This file shows proper error handling for both accepted and rejected 0-RTT scenarios and illustrates how to fall back to standard connection establishment when resumption is unavailable.
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 →