# Implementing Protocol Routing with Router and ProtocolHandler in iroh

> Learn to implement protocol routing in iroh using Router and ProtocolHandler for extensible peer-to-peer networking. Multiplex QUIC connections with ALPN identifiers.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-18

---

**The iroh router multiplexes incoming QUIC connections to user-defined protocol handlers using Application-Layer Protocol Negotiation (ALPN) identifiers, enabling extensible peer-to-peer networking without modifying core networking code.**

The iroh crate provides a flexible, asynchronous P2P networking stack built around a central routing mechanism. By implementing the `ProtocolHandler` trait and registering handlers with the `Router`, developers can define custom protocols that automatically dispatch based on ALPN identifiers. This architecture separates transport concerns from application logic, allowing you to compose complex networking behaviors through modular components.

## Core Components of Protocol Routing

The protocol routing system in iroh consists of several tightly integrated components that work together to dispatch connections efficiently.

### Endpoint

The **Endpoint** (located in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)) serves as the foundation of the networking stack. It holds the QUIC listener, maintains knowledge of its own node ID, and advertises the ALPNs it supports. The Endpoint acts as the entry point for both inbound and outbound connections, handling the low-level QUIC protocol details.

### Router and RouterBuilder

The **Router** (defined at line 97 in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) owns an `Endpoint` and runs the main accept loop that dispatches incoming connections. It uses a `ProtocolMap` to route traffic to the appropriate handler. The **RouterBuilder** (line 104 in the same file) provides a fluent API for configuring the router before it starts. Through the builder, you register protocol handlers, set optional incoming filters, and finally spawn the accept loop with `spawn()`.

### ProtocolHandler Trait

The **ProtocolHandler** trait (line 27 in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) defines the interface that all protocol implementations must satisfy. It requires an `accept` method that receives a `Connection` and returns a `Result<(), AcceptError>`. Handlers can also implement `shutdown` to define custom cleanup logic when the router terminates.

### Type Erasure and ProtocolMap

To store heterogeneous handler types in a single collection, iroh uses **DynProtocolHandler** (line 31) as a type-erased wrapper. The router stores these in a **ProtocolMap** (line 75), which is a `BTreeMap<Vec<u8>, Box<dyn DynProtocolHandler>>` mapping ALPN byte strings to their corresponding handlers. This design allows the router to maintain a homogeneous collection while preserving each handler's concrete async semantics.

## How Connection Routing Works

The routing process follows a strict lifecycle that ensures type-safe dispatch and graceful error handling.

### 1. Builder Configuration

You begin by creating a `RouterBuilder` via `Router::builder(endpoint)`. For each protocol you wish to serve, call `accept(alpn, handler)`, passing the ALPN identifier as a byte string and a type implementing `ProtocolHandler`. Internally, the builder inserts this pair into the `ProtocolMap` (see line 89 in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)). You may also optionally add an `incoming_filter` to reject or retry connections before the TLS handshake occurs.

### 2. Spawning the Accept Loop

Calling `spawn()` consumes the builder and performs several initialization steps: it registers the supported ALPNs on the endpoint via `endpoint.set_alpns`, creates a `JoinSet` for tracking connection tasks, and initializes a `CancellationToken`. The method then starts an async loop (`run_loop_fut`) that continuously:

- Waits for `endpoint.accept()` to yield a new `Incoming` connection
- Applies the optional filter, potentially calling `retry()`, `refuse()`, or `ignore()`
- Looks up the handler using the connection's ALPN via `protocols.get(&alpn)`
- Calls `handler.on_accepting` to obtain a `Connection`, then executes `handler.accept(connection)` in its own task

### 3. Connection Handling

The internal `handle_connection` function (near the end of [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) coordinates the handoff. It extracts the ALPN from the incoming connection, retrieves the corresponding handler from the map, and invokes the acceptance sequence. Errors during acceptance are logged but do not crash the router, ensuring that malformed connections cannot destabilize the entire node.

### 4. Graceful Shutdown

When `Router::shutdown` is called, the system triggers the cancellation token to stop the main accept loop. The router then awaits the abort-on-drop task, calls `protocols.shutdown()` to let every handler run its own shutdown future, cancels any remaining `accept` futures, and finally closes the endpoint. This ensures no resources leak and active connections receive proper cleanup.

## Implementing Custom Protocol Handlers

The following examples demonstrate how to implement `ProtocolHandler` for common use cases.

### Minimal Echo Protocol

This example implements a simple bidirectional echo server that copies data from the receive stream back to the send stream:

```rust
use iroh::{
    endpoint::{presets, Endpoint},
    protocol::{AcceptError, ProtocolHandler, Router},
    Connection,
};

#[derive(Debug, Clone)]
struct Echo;

#[iroh::async_trait]
impl ProtocolHandler for Echo {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = connection.accept_bi().await?;
        tokio::io::copy(&mut recv, &mut send).await?;
        send.finish()?;
        connection.closed().await;
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Bind a local endpoint with the default preset.
    let endpoint = Endpoint::bind(presets::N0).await?;

    // Build a router that serves the echo protocol on `/iroh/echo/1`.
    let router = Router::builder(endpoint)
        .accept(b"/iroh/echo/1", Echo)
        .spawn();

    // Keep the process alive until Ctrl-C, then shut down cleanly.
    tokio::signal::ctrl_c().await?;
    router.shutdown().await?;
    Ok(())
}

```

The key steps are: `Router::builder` creates the builder, `accept` registers the protocol, `spawn` starts the accept loop, and `shutdown` performs graceful termination.

### Filtering Incoming Connections

You can reject connections before the expensive TLS handshake by installing an incoming filter:

```rust
use std::sync::Arc;
use iroh::{
    protocol::{IncomingFilterOutcome, Router},
    endpoint::{presets, Endpoint},
};

fn ipv4_filter(incoming: &iroh::endpoint::Incoming) -> IncomingFilterOutcome {
    match incoming.remote_addr() {
        iroh::endpoint::IncomingAddr::Ip(addr) if addr.is_ipv4() => IncomingFilterOutcome::Accept,
        _ => IncomingFilterOutcome::Reject,
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = Endpoint::bind(presets::Minimal).await?;

    let router = Router::builder(endpoint)
        .incoming_filter(Arc::new(ipv4_filter))
        .accept(b"/iroh/echo/1", Echo)
        .spawn();

    router.shutdown().await?;
    Ok(())
}

```

This filter runs **before** the TLS handshake, saving CPU cycles when the client should be rejected outright based on network address.

### Custom Shutdown Logic

Protocol handlers can implement custom cleanup by overriding the `shutdown` method:

```rust
use std::sync::{Arc, Mutex};
use iroh::{
    protocol::{AcceptError, ProtocolHandler, Router},
    Connection,
};

#[derive(Debug, Default)]
struct Graceful {
    connections: Arc<Mutex<Vec<Connection>>>,
}

#[iroh::async_trait]
impl ProtocolHandler for Graceful {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
        self.connections.lock().unwrap().push(conn);
        Ok(())
    }

    async fn shutdown(&self) {
        // Give in-flight connections a chance to finish.
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        for conn in self.connections.lock().unwrap().drain(..) {
            conn.close(42u32.into(), b"shutdown");
        }
    }
}

```

Register this with `Router::builder(...).accept(b"/my/proto/1", Graceful::default())`. When `router.shutdown()` is called, the handler's `shutdown` runs first, allowing it to close its connections cleanly before the endpoint closes.

## Key Implementation Files

The protocol routing implementation spans several files in the iroh repository:

- [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) - Contains the full implementation of `Router`, `RouterBuilder`, the `ProtocolHandler` trait, and the accept-loop logic.
- [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) - Provides the low-level QUIC endpoint abstraction, including `Endpoint`, `Incoming`, and `Connection` types.
- [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs) - Demonstrates a minimal router setup with the echo protocol.
- [`iroh/tests/patchbay/switch-uplink.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/switch-uplink.rs) - Integration test showing router usage with multiple presets and filters.

These files illustrate how the router abstracts protocol routing, how a user supplies handlers, and how the system maintains clean shutdown semantics.

## Summary

- **Router** uses ALPN identifiers to dispatch incoming QUIC connections to registered **ProtocolHandler** implementations.
- The **RouterBuilder** allows configuration of protocols via `accept()` and optional pre-handshake filtering via `incoming_filter()`.
- **ProtocolHandler** requires implementing `accept()` for connection handling and optionally `shutdown()` for custom cleanup.
- The router stores handlers as `Box<dyn DynProtocolHandler>` in a `ProtocolMap`, enabling type-erased storage of heterogeneous protocol implementations.
- Graceful shutdown ensures all handlers receive cleanup time before the endpoint closes, preventing resource leaks.

## Frequently Asked Questions

### What is the relationship between Router and Endpoint in iroh?

The **Router** owns an **Endpoint** and manages its lifecycle. While the Endpoint handles the low-level QUIC protocol details and connection acceptance, the Router adds the multiplexing layer that inspects ALPN identifiers and dispatches connections to the appropriate ProtocolHandler. According to the iroh source code in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the Router calls `endpoint.accept()` in its main loop and registers supported ALPNs via `endpoint.set_alpns`.

### How does the Router determine which ProtocolHandler to use for an incoming connection?

The Router extracts the ALPN identifier from the incoming connection's TLS handshake and performs a lookup in its internal `ProtocolMap` (a `BTreeMap<Vec<u8>, Box<dyn DynProtocolHandler>>`). As implemented in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the router calls `protocols.get(&alpn)` to find the matching handler, then invokes `handler.accept(connection)` to process the connection.

### Can I reject connections before the TLS handshake completes?

Yes. The RouterBuilder supports an `incoming_filter` callback that runs before the TLS handshake. This filter receives an `Incoming` reference and returns an `IncomingFilterOutcome` indicating whether to accept, reject, ignore, or retry the connection. This allows for cheap firewalling or rate limiting without wasting CPU cycles on cryptographic operations.

### What happens to active connections when I call Router::shutdown?

When `shutdown()` is called, the Router first cancels the main accept loop to stop accepting new connections. It then calls `shutdown()` on each registered ProtocolHandler, giving them time to clean up active connections. Finally, it closes the Endpoint and aborts any remaining tasks. This ensures that existing connections can finish gracefully rather than being forcefully terminated.