ZeroRTT Connection Establishment in iroh: How It Works and How to Use It

ZeroRTT connection establishment in iroh allows clients to send application data immediately without waiting for the TLS handshake to complete, using QUIC's 0-RTT mode via the underlying noq library.

In peer-to-peer networking, reducing connection latency is critical for performance. The iroh library implements ZeroRTT (0-RTT) connection establishment on top of QUIC, enabling clients to transmit data in the first packet when session resumption state is available.

How ZeroRTT Works in iroh

The implementation centers on the Connecting and ZeroRttConnection types defined in iroh/src/endpoint/connection.rs. When a client has previously connected to a remote endpoint, it can skip the round-trip handshake and begin sending data immediately.

Initiating a ZeroRTT Connection

The process begins with Endpoint::connect_with_opts, which returns a Connecting future. To attempt 0-RTT, the caller invokes Connecting::into_0rtt at lines 92-100 of connection.rs:

let connecting = endpoint
    .connect_with_opts(remote_id, alpn, Default::default())
    .await?;

// Attempt to convert into 0-RTT mode
let zrtt_conn = match connecting.into_0rtt() {
    Ok(conn) => conn,  // 0-RTT available
    Err(conn) => {
        // Fallback to standard handshake
        let standard_conn = conn.await?;
        standard_conn
    }
};

If session resumption state exists on both sides, into_0rtt() returns an OutgoingZeroRttConnection. If not, it returns Err(Connecting), forcing a standard handshake.

Sending Early Data

Once obtained, the OutgoingZeroRttConnection allows immediate stream creation via open_bi() or open_uni() before the cryptographic handshake completes. As noted in lines 21-23 of connection.rs, streams expose an is_0rtt() method so applications can detect when they are operating in reduced-security mode.

// Open a bidirectional stream immediately
let (send, recv) = zrtt_conn.open_bi().await?;
assert!(send.is_0rtt());  // True until handshake completes

Handling Handshake Completion

The critical component managing the transition is OutgoingZeroRttConnection::handshake_completed(), implemented at lines 124-130. This method awaits the internal zrtt_accepted future and returns a ZeroRttStatus enum:

  • ZeroRttStatus::Accepted(conn): The server accepted the early data; the connection upgrades to the standard Connection type.
  • ZeroRttStatus::Rejected(conn): The server discarded the 0-RTT data; the connection is valid but any streams opened during 0-RTT are aborted.
match zrtt_conn.handshake_completed().await? {
    ZeroRttStatus::Accepted(conn) => {
        // Early data kept; continue using `conn`
    }
    ZeroRttStatus::Rejected(conn) => {
        // Must re-send data; previous streams aborted
    }
}

Server-Side ZeroRTT Acceptance

On the server side, incoming connections are wrapped in an Accepting struct. Calling Accepting::into_0rtt yields an IncomingZeroRttConnection that receives early data if the client sent any (lines 93-106). The server processes the data immediately but retains the ability to reject it later, which propagates to the client as described above.

Security Considerations for ZeroRTT

According to the documentation in connection.rs lines 16-20, 0-RTT data is vulnerable to replay attacks. Because the data is sent before the TLS handshake completes, an attacker could replay these packets. Applications must ensure that any operations performed during 0-RTT are idempotent. Non-idempotent operations must wait for handshake_completed() to return ZeroRttStatus::Accepted.

Implementation Example

The iroh/examples/0rtt.rs file demonstrates a complete client-server implementation.

Client-Side ZeroRTT Connection

This example shows the conditional logic for handling both accepted and rejected 0-RTT scenarios:

let connecting = endpoint
    .connect_with_opts(remote_id, PINGPONG_ALPN, Default::default())
    .await?;

let connection = match connecting.into_0rtt() {
    Ok(zrtt_conn) => {
        // Start work immediately
        let (send, recv) = zrtt_conn.open_bi().await?;
        let early_task = tokio::spawn(send_data(send));
        
        match zrtt_conn.handshake_completed().await? {
            ZeroRttStatus::Accepted(conn) => {
                early_task.await?;  // Data was kept
                conn
            }
            ZeroRttStatus::Rejected(conn) => {
                early_task.abort();  // Discard rejected work
                // Re-establish streams on validated connection
                let (s, r) = conn.open_bi().await?;
                send_data(s).await?;
                conn
            }
        }
    }
    Err(conn) => {
        // Standard handshake required
        conn.await?
    }
};

Server-Side ZeroRTT Handling

Servers enable 0-RTT by calling into_0rtt() on the Accepting struct:

while let Some(incoming) = endpoint.accept().await {
    tokio::spawn(async move {
        let accepting = incoming.accept()?;
        let connection = accepting.into_0rtt();
        let (mut send, mut recv) = connection.accept_bi().await?;
        
        // Check if data arrived before handshake
        if recv.is_0rtt() {
            println!("Received early data");
        }
        
        let data = recv.read_to_end(1024).await?;
        send.write_all(&data).await?;
        send.finish().await?;
        Ok::<_, n0_error::Error>(())
    });
}

Summary

  • ZeroRTT connection establishment eliminates handshake latency by allowing immediate data transmission using cached TLS session state.
  • Connecting::into_0rtt in iroh/src/endpoint/connection.rs initiates the process, returning either an OutgoingZeroRttConnection or falling back to standard handshake.
  • Early streams opened during 0-RTT must be considered insecure until handshake_completed() confirms acceptance via ZeroRttStatus::Accepted.
  • Replay protection is the application's responsibility; only idempotent operations should occur before handshake validation.
  • Server implementation uses Accepting::into_0rtt to receive early data, with rejection propagating to the client's status check.

Frequently Asked Questions

What is required for ZeroRTT to work in iroh?

ZeroRTT requires that the client has previously connected to the same remote endpoint and cached the TLS session resumption state. The server must also retain the corresponding state and be configured to allow session resumption. If either side lacks this state, as implemented in iroh/src/tls/resolver.rs, Connecting::into_0rtt returns an error and the connection falls back to a standard handshake.

What happens if the server rejects ZeroRTT data?

When the server rejects 0-RTT data, the client's handshake_completed() method returns ZeroRttStatus::Rejected(conn) where conn is a valid Connection object. According to the logic in iroh/src/endpoint/connection.rs lines 124-130, any streams opened during the 0-RTT phase are automatically aborted, and subsequent operations on those streams return a ZeroRttRejected error. The application must re-send the data over the new, validated connection.

How does iroh handle replay attacks in ZeroRTT mode?

Iroh does not provide automatic replay protection for 0-RTT data. As documented in connection.rs lines 16-20, the library explicitly warns that 0-RTT data is vulnerable to replay attacks. The application layer must ensure that any operations performed during the 0-RTT phase are idempotent, or defer non-idempotent operations until after handshake_completed() confirms the connection is accepted.

Can all types of streams use ZeroRTT in iroh?

Yes, both bidirectional (open_bi) and unidirectional (open_uni) streams support ZeroRTT mode. The OutgoingZeroRttConnection exposes these methods identically to a standard connection. The is_0rtt() method on the stream objects allows the application to distinguish between early data (sent before handshake completion) and data sent after the connection is fully validated.

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 →