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

> Learn how to use the iroh Router to multiplex multiple protocols on one endpoint. Register handlers by ALPN and let the router automatically direct connections for efficient network management.

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

---

**Use `Router::builder(endpoint)` to register multiple protocol handlers by their ALPN identifiers, then call `.spawn()` to start a single accept loop that automatically routes incoming QUIC connections to the correct handler based on the handshake negotiation.**

The `iroh` crate from the `n0-computer/iroh` repository provides a high-performance networking stack for building distributed applications. To serve multiple distinct application protocols over one QUIC endpoint, you use the **Router** to multiplex traffic based on Application-Layer Protocol Negotiation (ALPN) bytes exchanged during the initial handshake.

## Understanding the Router Architecture

`iroh::protocol::Router` acts as a thin, asynchronous dispatch layer that sits atop a single `Endpoint`. When a peer connects, the router extracts the ALPN string from the QUIC handshake and forwards the connection to the corresponding protocol handler registered in an internal `ProtocolMap`.

### How ALPN-Based Dispatch Works

During the QUIC handshake, the client and server negotiate an application protocol using ALPN bytes. The router leverages this to route traffic without manual inspection of the connection stream. In [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the `handle_connection` function (lines 642-648) implements this logic:

1. Accepts the raw QUIC connection via `endpoint.accept()`.
2. Extracts the ALPN bytes from the handshake metadata.
3. Looks up the matching handler in the `ProtocolMap`.
4. Spawns a task to run the handler's `accept` or `on_accepting` callback.

### The Accept Loop and Task Management

The router runs a single asynchronous accept loop in a dedicated task. This loop manages all protocol handlers using a `JoinSet`, ensuring that if any handler panics, the router aborts the entire loop to prevent undefined states. The endpoint remains owned by the router, enabling clean shutdown semantics that cancel pending tasks and close connections in one step.

## Step-by-Step Implementation

To multiplex protocols, you construct a `RouterBuilder`, register each protocol with its unique ALPN, and spawn the router into a background task.

### Building the Router

Start by binding an endpoint and creating a builder. In [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the `Router::builder` function initializes the `RouterBuilder` struct:

```rust
let endpoint = Endpoint::bind(presets::N0).await?;
let builder = Router::builder(endpoint);

```

### Registering Protocol Handlers

For each protocol you wish to expose, call `RouterBuilder::accept` with the ALPN bytes and a handler implementing the `ProtocolHandler` trait. According to the source at lines 85-92, each call inserts a new entry into the internal map:

```rust
const ECHO_ALPN: &[u8] = b"/iroh/example/echo/1";
const TIME_ALPN: &[u8] = b"/iroh/example/time/1";

let builder = Router::builder(endpoint)
    .accept(ECHO_ALPN, Echo)   // First protocol
    .accept(TIME_ALPN, Time);  // Second protocol

```

Handlers must implement `ProtocolHandler`, which requires an `accept` method that receives the `Connection` object. You may wrap handlers in `Arc<...>` or `Box<...>` if needed.

### Spawning the Accept Loop

Call `spawn()` to install the ALPN list on the endpoint and start the accept loop. As implemented in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) at lines 99-107, this returns a cloneable `Router` handle:

```rust
let router = builder.spawn();

```

Once spawned, the router begins listening for incoming connections and automatically dispatches them based on the negotiated ALPN.

## Complete Example: Echo and Time Protocols

The following example demonstrates running two distinct protocols on a single endpoint. The **Echo** protocol mirrors data back to the client, while the **Time** protocol returns the current UNIX timestamp.

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

const ECHO_ALPN: &[u8] = b"/iroh/example/echo/1";
const TIME_ALPN: &[u8] = b"/iroh/example/time/1";

#[tokio::main]
async fn main() -> Result<()> {
    // Build and spawn the router with both protocols
    let router = start_server().await?;
    
    // Wait for the endpoint to become reachable
    router.endpoint().online().await;

    // Connect to both protocols on the same endpoint
    connect_and_use(router.endpoint().addr(), ECHO_ALPN, b"ping").await?;
    connect_and_use(router.endpoint().addr(), TIME_ALPN, b"").await?;

    // Graceful shutdown
    router.shutdown().await.anyerr()?;
    Ok(())
}

async fn start_server() -> Result<Router> {
    let endpoint = Endpoint::bind(presets::N0).await?;
    let router = Router::builder(endpoint)
        .accept(ECHO_ALPN, Echo)
        .accept(TIME_ALPN, Time)
        .spawn();
    Ok(router)
}

#[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(())
    }
}

#[derive(Debug, Clone)]
struct Time;
impl ProtocolHandler for Time {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
        let (mut send, _) = conn.accept_bi().await?;
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();
        send.write_all(format!("now={}\n", now).as_bytes()).await?;
        send.finish()?;
        conn.closed().await;
        Ok(())
    }
}

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!("ALPN {}: {}", std::str::from_utf8(alpn)?, String::from_utf8_lossy(&resp));
    
    conn.close(0u32.into(), b"done");
    client.close().await;
    Ok(())
}

```

This pattern scales to **any number of protocols**—simply continue chaining `.accept(alpn, handler)` calls before `.spawn()`.

## Graceful Shutdown

To shut down the router without dropping active connections, call `Router::shutdown`. As implemented in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) at lines 404-452, this method:

1. Cancels the main accept loop.
2. Signals all registered protocol handlers to shut down.
3. Waits for all handler tasks to complete.
4. Closes the underlying endpoint.

```rust
router.shutdown().await?;

```

This ensures that in-flight protocol operations complete before the network resources are released.

## Summary

- **Single endpoint, multiple protocols**: The `Router` allows one `Endpoint` to serve any number of application protocols distinguished by ALPN bytes.
- **Registration**: Use `Router::builder(endpoint).accept(alpn, handler)` to map ALPN identifiers to `ProtocolHandler` implementations.
- **Spawn**: Calling `.spawn()` starts the internal accept loop that multiplexes connections based on the QUIC handshake.
- **Source locations**: Core logic resides in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), specifically the `RouterBuilder::accept` method (lines 85-92), `RouterBuilder::spawn` (lines 99-107), and `handle_connection` dispatch (lines 642-648).
- **Cleanup**: Use `router.shutdown().await` for graceful termination that properly cleans up all handler tasks and the endpoint.

## Frequently Asked Questions

### What is ALPN and why is it required for the Router?

ALPN (Application-Layer Protocol Negotiation) is a TLS extension used by QUIC to advertise supported protocols during the handshake. The Router uses these bytes to identify which handler should receive a connection without reading application data first. Without ALPN, the router would have no mechanism to determine which protocol implementation should handle the connection.

### How many protocols can a single Router instance handle?

There is no hardcoded limit within the Router itself. You may register any number of protocols using consecutive `.accept()` calls on the builder. Each registration inserts a new entry into the internal `ProtocolMap`, which is consulted for every incoming connection. Performance depends only on your endpoint's capacity to handle concurrent QUIC connections.

### What happens if a client connects with an unregistered ALPN?

If a client negotiates an ALPN that was not registered with the router, the router will reject the connection during the handshake phase. The connection never reaches your application code, as the dispatch logic in `handle_connection` only forwards connections with matching ALPN entries found in the `ProtocolMap`.

### Can I filter connections before they reach the protocol handlers?

Yes. The `RouterBuilder` supports an optional `IncomingFilter` set via `RouterBuilder::incoming_filter`. This filter runs after the handshake completes but before the connection is dispatched to the handler, allowing you to reject, retry, or ignore connections based on custom logic such as IP allowlists or rate limiting.