# How to Use the iroh Router to Compose Multiple Protocols on a Single Endpoint

> Learn how to use the iroh Router to compose multiple protocols on a single endpoint by dispatching incoming connections to registered handlers based on their ALPN identifier.

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

---

**The iroh `Router` multiplexes any number of application-level protocols over a single QUIC `Endpoint` by dispatching incoming connections to registered handlers based on their ALPN identifier.**

The `iroh::protocol::Router` in the n0-computer/iroh repository provides a zero-cost abstraction for running multiple protocols concurrently on one network address. By leveraging Application-Layer Protocol Negotiation (ALPN) bytes exchanged during the QUIC handshake, the Router eliminates the need for separate ports or complex manual connection routing.

## Architecture of the Protocol Router

The Router operates as a thin, high-performance layer above the raw QUIC `Endpoint`. It maintains an internal `ProtocolMap` that associates ALPN byte strings with their respective handlers, then runs a dedicated asynchronous task to accept and dispatch connections.

### The ALPN Dispatch Loop

Inside [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the `handle_connection` function (lines 642-648) implements the core dispatch logic:

1. **Accepts** the raw QUIC connection via `endpoint.accept()`.
2. **Extracts** the ALPN string from the TLS handshake metadata.
3. **Looks up** the corresponding handler in the `ProtocolMap`.
4. **Spawns** a new task that executes the handler's `accept` method.

If any handler panics, the Router aborts the entire loop using a `JoinSet`, preventing the system from entering a half-initialized state.

### Protocol Registration

Before spawning, you register handlers using `RouterBuilder::accept` (lines 85-92 in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)). Each call inserts a new entry into the internal map:

```rust
Router::builder(endpoint)
    .accept(b"/my/proto/1", handler_one)
    .accept(b"/my/proto/2", handler_two)

```

The handler must implement the `ProtocolHandler` trait, typically wrapped in an `Arc` or `Box` for thread-safe sharing.

## Building a Multi-Protocol Router

Constructing a Router that serves multiple protocols follows a strict builder pattern. You initialize the router with an existing `Endpoint`, register one or more protocol handlers, and then spawn the accept loop.

### Step-by-Step Implementation

1. **Bind the endpoint** using your preferred transport preset (e.g., `presets::N0`).
2. **Create the builder** with `Router::builder(endpoint)`.
3. **Register handlers** by calling `.accept(alpn, handler)` for each protocol.
4. **Spawn the router** with `.spawn()` to start the accept loop and return a `Router` handle.
5. **Optional:** Set an `IncomingFilter` via `RouterBuilder::incoming_filter` to reject connections before the handshake completes.

### Complete Rust Example

Below is a complete example demonstrating an Echo protocol and a Time protocol running simultaneously on the same endpoint. This pattern mirrors the official examples in [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs) and [`iroh/examples/incoming-filter.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/incoming-filter.rs).

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

/// ALPN identifiers for the two protocols.
const ECHO_ALPN: &[u8] = b"/iroh/example/echo/1";
const TIME_ALPN: &[u8] = b"/iroh/example/time/1";

#[tokio::main]
async fn main() -> Result<()> {
    // ---- Server side -------------------------------------------------------
    let router = start_server().await?;

    // Wait until the endpoint is reachable (e.g. NAT hole‑punched).
    router.endpoint().online().await;

    // ---- Client side -------------------------------------------------------
    // Connect to the same endpoint twice, once for each protocol.
    connect_and_use(router.endpoint().addr(), ECHO_ALPN, b"ping").await?;
    connect_and_use(router.endpoint().addr(), TIME_ALPN, b"").await?;

    // Clean shutdown – all handler tasks finish gracefully.
    router.shutdown().await.anyerr()?;
    Ok(())
}

// ---------------------------------------------------------------------------
// Server side: build a router that knows *both* protocols.
async fn start_server() -> Result<Router> {
    let endpoint = Endpoint::bind(presets::N0).await?;
    let router = Router::builder(endpoint)
        .accept(ECHO_ALPN, Echo)   // first protocol
        .accept(TIME_ALPN, Time)   // second protocol
        .spawn();                  // start the accept loop
    Ok(router)
}

// ---------------------------------------------------------------------------
// Simple echo handler – identical to the example in the repo.
#[derive(Debug, Clone)]
struct Echo;
impl ProtocolHandler for Echo {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = conn.accept_bi().await?;
        tokio::io::copy(&mut recv, &mut send).await?;
        send.finish()?;
        conn.closed().await;
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// A tiny “time” protocol: on each connection it writes the current UNIX
// timestamp and then closes.
#[derive(Debug, Clone)]
struct Time;
impl ProtocolHandler for Time {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
        use std::time::{SystemTime, UNIX_EPOCH};
        let (mut send, _) = conn.accept_bi().await?;
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();
        let payload = format!("now={now}\n");
        send.write_all(payload.as_bytes()).await?;
        send.finish()?;
        conn.closed().await;
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// Client helper – opens a connection, sends optional data, prints the response.
async fn connect_and_use(addr: EndpointAddr, alpn: &[u8], outbound: &[u8]) -> Result<()> {
    let client = Endpoint::bind(presets::N0).await?;
    let conn = client.connect(addr, alpn).await?;
    let (mut send, mut recv) = conn.open_bi().await?;
    if !outbound.is_empty() {
        send.write_all(outbound).await?;
    }
    send.finish()?;
    let resp = recv.read_to_end(1024).await?;
    println!("Response for ALPN {}: {}", std::str::from_utf8(alpn)?, String::from_utf8_lossy(&resp));
    conn.close(0u32.into(), b"done");
    client.close().await;
    Ok(())
}

```

In this implementation, the client connects to the same `EndpointAddr` twice using different ALPN strings. The Router automatically routes each connection to the appropriate handler without additional configuration.

## Graceful Shutdown and Error Handling

The Router provides clean lifecycle management through the `shutdown` method (lines 404-452 in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)). When you call `router.shutdown().await`:

- The accept loop is cancelled immediately.
- All pending handler tasks are notified and awaited via the internal `JoinSet`.
- The underlying `Endpoint` is closed.

This ensures that no connections are dropped mid-handshake and that all protocol handlers complete their current work before the application exits.

## Summary

- **Initialize** the Router with `Router::builder(endpoint)` to obtain a `RouterBuilder`.
- **Register** each protocol using `.accept(alpn, handler)`, ensuring every ALPN identifier is unique.
- **Spawn** the router with `.spawn()` to start the ALPN-based dispatch loop and expose all registered protocols on the single endpoint.
- **Shutdown** gracefully by awaiting `router.shutdown()` to ensure all handler tasks and the endpoint close cleanly.

## Frequently Asked Questions

### What is the maximum number of protocols the iroh Router can handle?

The Router stores protocols in a standard `ProtocolMap` (a hash map), so the practical limit depends on available memory and the uniqueness of your ALPN identifiers. Each protocol requires a distinct ALPN byte sequence, and there is no hardcoded limit in the implementation.

### How does the Router handle connections with unknown ALPN identifiers?

According to the dispatch logic in `handle_connection` within [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), if the ALPN extracted from the handshake does not match any registered handler, the connection is rejected. You can also configure an `IncomingFilter` via `RouterBuilder::incoming_filter` to inspect and reject connections before the handshake completes.

### Can I add protocols to a running Router?

No. The ALPN list is fixed when you call `spawn()` (lines 99-107 in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)). The method installs the ALPN list on the endpoint and starts the accept loop. To add new protocols, you must create a new `RouterBuilder`, register all desired protocols, and spawn a new Router instance.

### Does each protocol handler run in its own task?

Yes. When `handle_connection` dispatches a connection, it spawns the handler's `accept` method in a new asynchronous task managed by a `JoinSet`. This isolation ensures that blocking operations or panics in one handler do not impact the main accept loop or other running protocols.