# How Iroh's Connection State Machine Manages the Connection Lifecycle

> Learn how iroh's connection state machine manages the connection lifecycle through distinct phases. Prevent invalid operations with this compile-time type-state pattern.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: internals
- Published: 2026-06-19

---

**Iroh implements a compile-time type-state pattern in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) that explicitly models the connection lifecycle through three distinct phases—Connecting/Accepting, 0-RTT, and HandshakeCompleted—preventing invalid operations at each stage.**

Iroh builds its networking stack on top of the QUIC library **noq**, exposing a generic `Connection<State>` struct that enforces valid state transitions at compile time. The **iroh connection state machine** splits the connection lifecycle into three main compile-time states defined in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), ensuring that methods like opening streams or querying remote identity are only available after the appropriate handshake phase completes.

## The Type-State Architecture

The core of the state machine is a generic struct parameterized by a marker trait:

```rust
pub struct Connection<State: ConnectionState = HandshakeCompleted> {
    inner: noq::Connection,
    data: State::Data,
}

```

The `ConnectionState` trait is sealed, with only three implementations defined in the same file:

- **`Connecting`** – Exists after `Endpoint::connect()` but before the TLS handshake finishes
- **`Accepting`** – Exists after an `Incoming` connection is accepted but before the handshake completes  
- **`HandshakeCompleted`** – The fully established connection state

Additional marker structs `IncomingZeroRtt` and `OutgoingZeroRtt` handle 0-RTT variants, ensuring that early data operations are type-safe.

## Outgoing Connection Lifecycle

Outgoing connections begin with `Endpoint::connect()`, which returns a `Connecting` future. According to the implementation in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (lines 40-46 and 71-78), polling this future resolves the underlying `noq::Connecting` and registers the connection with the endpoint's socket layer via `conn_from_noq_conn`:

```rust
let fut = conn_from_noq_conn(noq_conn, &self.ep)?;
self.register_with_socket = Some(Box::pin(fut.err_into()));

```

When the future completes, it yields a `Connection<HandshakeCompleted>`.

### Attempting 0-RTT

To enable 0-RTT (zero round-trip time) for outgoing connections, call `Connecting::into_0rtt()` (lines 528-548). This consumes the `Connecting` state and returns an `OutgoingZeroRttConnection` containing a shared future that resolves to `ZeroRttStatus::Accepted` or `ZeroRttStatus::Rejected`:

```rust
let conn = endpoint
    .connect(server_addr, b"/iroh/echo/1")
    .await?          // `Connecting`
    .into_0rtt()?    // `OutgoingZeroRttConnection`
    .handshake_completed()
    .await?;         // `ZeroRttStatus`

match conn {
    ZeroRttStatus::Accepted(c) => println!("0-RTT accepted, remote ID {}", c.remote_id()),
    ZeroRttStatus::Rejected(c) => println!("0-RTT rejected, fall-back connection {}", c.remote_id()),
}

```

If rejected, streams opened during the 0-RTT phase must be recreated.

## Incoming Connection Lifecycle

Incoming connections follow a path from `Incoming` to `Accepting` to `HandshakeCompleted`. The router in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) (lines 624-647) manages this via the `handle_connection` loop, which calls `incoming.accept()` to create an `Accepting` state.

Protocol handlers can intercept connections before the handshake completes by implementing `on_accepting`:

```rust
use iroh::protocol::{ProtocolHandler, AcceptError};

struct MyHandler;
impl ProtocolHandler for MyHandler {
    async fn on_accepting(
        &self,
        accepting: Accepting,
    ) -> Result<Connection, AcceptError> {
        // Turn the incoming connection into a 0-RTT connection
        let zrtt_conn = accepting.into_0rtt();
        
        // Use the connection immediately (streams available before handshake)
        let (mut send, mut recv) = zrtt_conn.accept_bi().await?;
        // … application logic …
        Ok(zrtt_conn)
    }
}

```

The `into_0rtt()` method on the server side (lines 600-626) always succeeds because servers can fall back to normal handshake if the client did not send 0-RTT data.

## Early Filtering and Rejection

Before the handshake completes, the router applies an **incoming filter** (lines 558-585 in [`protocol.rs`](https://github.com/n0-computer/iroh/blob/main/protocol.rs)). The filter returns an `IncomingFilterOutcome` that supports four actions:

- **Accept** – Continue normal processing
- **Retry** – Send a QUIC `RETRY` packet to force address validation (useful for NAT traversal)
- **Reject** – Send `CONNECTION_REFUSED` immediately
- **Ignore** – Silently drop the packet

```rust
let router = Router::builder(endpoint)
    .incoming_filter(Arc::new(|inc| {
        if inc.remote_addr_validated() {
            IncomingFilterOutcome::Accept
        } else {
            IncomingFilterOutcome::Reject
        }
    }))
    .accept(b"/my/proto/1", Echo)
    .spawn();

```

These actions are performed directly on the `Incoming` object via `incoming.retry()`, `incoming.refuse()`, or `incoming.ignore()`.

## Weak Handles and Graceful Shutdown

The state machine supports non-owning references via `WeakConnectionHandle` (lines 127-138). This allows code to monitor connection status without preventing the connection from closing naturally. When all strong `Connection` handles are dropped, the underlying QUIC connection closes, but weak handles can still be upgraded to check termination status or await closure during endpoint shutdown.

## Summary

- **Iroh's connection state machine** uses a type-state pattern in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) to enforce compile-time safety across connection phases.
- **Outgoing connections** transition from `Connecting` to `HandshakeCompleted`, optionally passing through `OutgoingZeroRtt`.
- **Incoming connections** move from `Incoming` to `Accepting` via the router's accept loop, supporting early interception via `on_accepting`.
- **0-RTT support** is available for both outgoing and incoming connections, with explicit `ZeroRttStatus` handling for rejected early data.
- **Early filtering** allows rejection, retry, or ignore actions before the TLS handshake completes.
- **Weak handles** enable monitoring and graceful shutdown without owning the connection lifecycle.

## Frequently Asked Questions

### What is the difference between `Connecting` and `Accepting` states in iroh?

`Connecting` represents an outgoing connection initiated by `Endpoint::connect()` that has not yet completed the TLS handshake, while `Accepting` represents an incoming connection that has been accepted from an `Incoming` object but is still completing the handshake. Both states prevent stream operations until the handshake finishes, but `Accepting` additionally supports the `into_0rtt()` conversion and early rejection via the incoming filter.

### How does iroh handle 0-RTT connection failures?

When a client attempts 0-RTT by calling `into_0rtt()`, the resulting `OutgoingZeroRttConnection` contains a shared future that resolves to `ZeroRttStatus::Accepted` or `ZeroRttStatus::Rejected`. If rejected, the connection still exists but streams opened during the 0-RTT phase are broken and must be recreated. Server-side 0-RTT always succeeds in conversion but may fall back to normal handshake if the client did not provide early data.

### Can I reject a connection before the TLS handshake completes?

Yes. The router supports an `incoming_filter` closure that receives the `Incoming` object and returns `IncomingFilterOutcome::Reject` to send `CONNECTION_REFUSED`, `Retry` to force a QUIC retry packet for address validation, or `Ignore` to silently drop the packet. This occurs in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) before the `Accepting` state is created.

### Why does iroh use a type-state pattern for connections?

The type-state pattern enforces the QUIC protocol rules at compile time, ensuring that methods like `open_bi()` or `remote_id()` are only callable on `Connection<HandshakeCompleted>`. This prevents runtime errors where an application might try to use a connection before the handshake finishes, and it eliminates the need for runtime state checks or unwraps during the connection lifecycle.