How 0-RTT Connection Establishment Works in iroh: A Complete Guide

iroh implements 0-RTT connection establishment by wrapping the noq QUIC stack and exposing Connecting::into_0rtt(), which attempts to resume previous TLS sessions and returns an OutgoingZeroRttConnection that resolves to either ZeroRttStatus::Accepted or ZeroRttStatus::Rejected after the handshake completes.

Understanding 0-RTT connection establishment in iroh is critical for building low-latency applications that need to send data immediately without waiting for a full TLS handshake. The iroh library, developed by n0-computer, leverages the noq QUIC implementation to provide zero-round-trip-time resumption while exposing a safe Rust API for handling session acceptance and rejection.

The QUIC Stack: noq and Session Resumption

iroh delegates its transport layer to the noq crate, which manages the underlying QUIC protocol and TLS 1.3 session tickets. When a client has previously connected to a server and obtained session resumption tickets, noq stores these credentials to enable subsequent 0-RTT attempts. The iroh crate wraps noq’s low-level primitives and provides the high-level Connecting and OutgoingZeroRttConnection types that application code interacts with.

The 0-RTT capability is exposed through the iroh::endpoint::Connecting struct. According to the source code in iroh/src/endpoint/connection.rs, the conversion to 0-RTT mode happens through the into_0rtt() method, which internally bridges to noq::Connecting::into_0rtt() at lines 92-102.

Step-by-Step 0-RTT Flow

1. Create the Client Endpoint

Begin by initializing an iroh::Endpoint bound to a local UDP socket. This endpoint manages the underlying QUIC state and connection pool.

use iroh::Endpoint;
use std::net::SocketAddr;

let client = Endpoint::client("0.0.0.0:0".parse::<SocketAddr>()?).await?;

2. Initiate the Connection

Call Endpoint::connect() with the remote address and server name. This returns a Connecting future that represents the in-progress handshake. At this stage, the QUIC stack checks its session cache for resumption tickets.

let connecting = client.connect("198.51.100.42:443".parse()?, "example.com").await?;

3. Attempt 0-RTT Conversion

Invoke into_0rtt() on the Connecting object. As implemented in iroh/src/endpoint/connection.rs (lines 92-102), this method attempts to resume the TLS session. It returns:

  • Ok(OutgoingZeroRttConnection) if 0-RTT is possible
  • Err(Connecting) if no valid session ticket exists, falling back to standard 1-RTT
let outgoing = match connecting.into_0rtt() {
    Ok(zrtt) => zrtt,     // Proceed with 0-RTT
    Err(conn) => conn,    // Fallback to normal handshake
};

4. Wait for Remote Acceptance

The OutgoingZeroRttConnection contains an internal future zrtt_accepted that resolves when the remote peer validates the session ticket. In iroh/src/endpoint/connection.rs (lines 129-140), this maps to noq returning a tuple (Connection, zrtt_accepted). The future yields a bool indicating whether the server accepted the 0-RTT data.

You await the final handshake status via handshake_completed() (lines 240-260), which returns a ZeroRttStatus enum.

5. Handle ZeroRttStatus

The ZeroRttStatus enum distinguishes between accepted and rejected scenarios:

  • ZeroRttStatus::Accepted(conn): The server validated the session. Streams opened before the handshake remain usable, and your early data was transmitted successfully.
  • ZeroRttStatus::Rejected(conn): The server rejected the session resumption. Any streams opened during the 0-RTT phase will error, and you must reopen them after the handshake completes.
match outgoing.handshake_completed().await? {
    iroh::endpoint::ZeroRttStatus::Accepted(conn) => {
        // 0-RTT data accepted, use existing streams
        let (mut send, recv) = conn.open_bi().await?;
    }
    iroh::endpoint::ZeroRttStatus::Rejected(conn) => {
        // Must retry streams after full handshake
        let (mut send, recv) = conn.open_bi().await?;
    }
}

Handling Acceptance and Rejection: ZeroRttStatus

The ZeroRttStatus enum defined in iroh/src/endpoint/connection.rs (lines 92-101) is the primary mechanism for handling the dual-outcome nature of 0-RTT. Because 0-RTT data is sent before cryptographic parameters are fully verified, applications must treat rejection gracefully.

When rejected, the underlying noq connection upgrades from 0-RTT to 1-RTT mode, but stream IDs allocated during the 0-RTT phase become invalid. The iroh API abstracts this complexity by ensuring that when ZeroRttStatus::Rejected is returned, the provided Connection object is fresh and ready for new streams, while any previous stream attempts receive explicit errors indicating they should be retried.

Practical Implementation Example

The repository includes a runnable demonstration in iroh/examples/0rtt.rs. Below is a condensed version showing the complete client-side flow:

use iroh::{Endpoint, Result};
use std::net::SocketAddr;

#[tokio::main]
async fn main() -> Result<()> {
    // Bind to an OS-chosen UDP port
    let client = Endpoint::client("0.0.0.0:0".parse::<SocketAddr>()?).await?;
    
    // Initiate connection (must have connected previously for 0-RTT)
    let connecting = client.connect("198.51.100.42:443".parse()?, "example.com")?;
    
    // Attempt 0-RTT conversion
    let outgoing = match connecting.into_0rtt() {
        Ok(zrtt) => zrtt,
        Err(connecting) => {
            println!("No session ticket available, using 1-RTT");
            // Wait for standard handshake if needed, or handle accordingly
            todo!()
        }
    };
    
    // Await handshake completion and check acceptance status
    match outgoing.handshake_completed().await? {
        iroh::endpoint::ZeroRttStatus::Accepted(conn) => {
            println!("0-RTT accepted");
            let mut stream = conn.open_uni().await?;
            stream.write(b"early data").await?;
        }
        iroh::endpoint::ZeroRttStatus::Rejected(conn) => {
            println!("0-RTT rejected - resending after handshake");
            let mut stream = conn.open_uni().await?;
            stream.write(b"data after handshake").await?;
        }
    }
    
    Ok(())
}

Key Source Files and Their Roles

File Purpose
iroh/src/endpoint/connection.rs Contains the Connecting::into_0rtt() implementation (lines 92-102), ZeroRttStatus enum definition, and the zrtt_accepted future handling (lines 129-148).
iroh/src/endpoint/quic.rs Configures QUIC transport parameters and session ticket handling that enable 0-RTT resumption.
iroh/examples/0rtt.rs Provides a complete, runnable client example demonstrating successful 0-RTT session resumption.
iroh-relay/src/quic.rs Implements the server-side QUIC listener with ALPN identifier /iroh-qad/0 and address discovery mechanisms relevant to session validation.

Summary

  • iroh uses the noq crate for underlying QUIC and TLS 1.3 session management, exposing 0-RTT through the Connecting::into_0rtt() method.
  • Conversion happens in iroh/src/endpoint/connection.rs lines 92-102, where the library attempts to resume previous sessions.
  • Two outcomes exist: ZeroRttStatus::Accepted allows immediate stream usage, while ZeroRttStatus::Rejected requires reopening streams after the full handshake.
  • Always await handshake_completed() (lines 240-260) to determine whether early data was accepted before relying on it.
  • Reference the iroh/examples/0rtt.rs file for production-ready patterns handling both acceptance and rejection scenarios.

Frequently Asked Questions

What is the difference between 0-RTT and 1-RTT in iroh?

0-RTT allows sending application data during the initial handshake using cached session keys, while 1-RTT completes the full TLS handshake before any data flows. In iroh, 0-RTT is achieved by calling into_0rtt() on a Connecting object, which attempts to resume a previous session. If no ticket exists or the server rejects resumption, the connection falls back to 1-RTT mode, requiring a complete cryptographic negotiation before data transmission.

How does iroh determine if 0-RTT data is accepted?

The remote peer validates the TLS session ticket and ALPN identifier. In iroh/src/endpoint/connection.rs (lines 129-140), the library awaits a zrtt_accepted future returned by the underlying noq stack. This future resolves to a boolean indicating whether the server accepted the resumed session. The handshake_completed() method then wraps this result into the ZeroRttStatus enum, providing a clean API for the application to handle the outcome.

What happens to streams opened before the handshake completes if 0-RTT is rejected?

Streams opened during the 0-RTT phase error out and must be retried. When ZeroRttStatus::Rejected is returned, the Connection object provided is valid and ready for new streams, but any streams initiated before the handshake are invalidated. According to the implementation in iroh/src/endpoint/connection.rs, applications must reopen streams after receiving the Rejected status to ensure data reaches the peer reliably.

Is 0-RTT secure for all types of data in iroh?

No, 0-RTT data is vulnerable to replay attacks and should not be used for state-changing operations without idempotency safeguards. While iroh handles the cryptographic session resumption correctly via noq, the library documentation (see README.md) warns that applications must treat 0-RTT data as potentially replayable. Sensitive or non-idempotent requests should be delayed until the ZeroRttStatus::Accepted confirmation is received or sent over the post-handshake 1-RTT connection.

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 →