# Building Multi-Protocol Servers with Router in iroh

> Learn to build multi-protocol servers with iroh Router. Handle multiple protocols simultaneously on a single QUIC Endpoint using ALPN. Explore early connection filtering and graceful shutdown.

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

---

**The iroh `Router` allows a single QUIC `Endpoint` to accept and route connections for multiple protocols simultaneously using ALPN identifiers, with support for early connection filtering and graceful shutdown.**

The n0-computer/iroh repository provides a Rust networking stack designed for peer-to-peer applications. Building multi-protocol servers with Router in iroh enables developers to multiplex different application protocols over a single QUIC endpoint, reducing operational complexity while maintaining clean separation of concerns between protocol implementations.

## Router Architecture and Core Components

The router implementation resides primarily in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), where the core types coordinate to manage connection lifecycle and protocol dispatch.

### RouterBuilder and Protocol Registration

The `RouterBuilder` serves as the configuration stage before the server begins accepting connections. According to the source code in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), calling `Router::builder(endpoint)` returns a builder instance (lines 405–407) that accumulates protocol handlers and optional configuration.

Key methods on `RouterBuilder` include:

- **`accept(alpn, handler)`** – Registers a `ProtocolHandler` for a specific ALPN byte string, storing the mapping in an internal `ProtocolMap` (lines 85–92).
- **`incoming_filter(filter)`** – Sets an `IncomingFilter` function to evaluate connections before the TLS handshake completes (lines 72–74).
- **`spawn()`** – Finalizes the builder, registers the complete ALPN list on the endpoint, and launches the background accept loop (lines 99–122).

### ProtocolHandler Trait

Each protocol implements the `ProtocolHandler` trait, which defines how the router handles incoming connections for a specific ALPN. The trait requires an `accept` method and optionally supports `on_accepting`:

```rust
#[async_trait::async_trait]
impl ProtocolHandler for MyHandler {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        // Protocol-specific logic here
        Ok(())
    }
}

```

When a client connects, the router extracts the ALPN from the TLS handshake (lines 33–40) and looks up the corresponding handler in the `ProtocolMap` (lines 42–45). If no handler matches, the connection is silently ignored (line 44).

### IncomingFilter for Early Gating

The `IncomingFilter` trait enables security and resource management decisions before committing to a full TLS handshake. The filter receives an `Incoming` connection reference and returns one of four `IncomingFilterOutcome` variants:

- **`Accept`** – Proceed with normal handshake and protocol selection.
- **`Retry`** – Send a QUIC retry token to validate the client's address (`incoming.retry()`), then skip further processing.
- **`Reject`** – Actively refuse the connection with a `CONNECTION_REFUSED` error.
- **`Ignore`** – Drop the connection silently, causing the client to time out.

The router evaluates the filter at lines 62–85 of [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), allowing you to reject unvalidated addresses or implement rate limiting before cryptographic operations occur.

## Creating a Multi-Protocol Server

To instantiate a router, bind an `Endpoint` and chain the builder methods to register your handlers:

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

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

#[tokio::main]
async fn main() -> Result<()> {
    let endpoint = Endpoint::bind(presets::N0).await?;
    
    let router = Router::builder(endpoint)
        .accept(b"/echo/1", Echo)
        .accept(b"/chat/1", ChatHandler)  // Additional protocol
        .spawn();
    
    // Server runs until explicit shutdown
    tokio::signal::ctrl_c().await?;
    router.shutdown().await?;
    Ok(())
}

```

This pattern, demonstrated in [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs), binds a single endpoint that simultaneously serves the `/echo/1` and `/chat/1` protocols, routing each connection to its respective handler based on the ALPN negotiated during the QUIC handshake.

## Implementing Protocol Handlers

Protocol handlers operate on `Connection` objects provided by the router. The handler fully owns the connection once the router delegates it, responsible for stream management and clean termination.

The echo server example in [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs) (lines 84–100) illustrates a complete implementation:

```rust
#[async_trait::async_trait]
impl ProtocolHandler for Echo {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        // Accept a bidirectional stream
        let (mut send, mut recv) = connection.accept_bi().await?;
        
        // Copy received data back to sender
        tokio::io::copy(&mut recv, &mut send).await?;
        send.finish()?;
        
        // Wait for connection close to ensure clean shutdown
        connection.closed().await;
        Ok(())
    }
}

```

Handlers should await `connection.closed()` or explicit stream closure to prevent resource leaks, as the router tracks active connections during shutdown.

## Filtering Connections Before TLS

Implement `IncomingFilter` to reject unwanted connections before cryptographic handshaking consumes resources. This is particularly useful for mitigating denial-of-service attacks or enforcing address validation:

```rust
use iroh::protocol::{IncomingFilterOutcome, IncomingFilter};

fn validation_filter(incoming: &iroh::endpoint::Incoming) -> IncomingFilterOutcome {
    if incoming.remote_addr_validated() {
        IncomingFilterOutcome::Accept
    } else {
        IncomingFilterOutcome::Retry  // Force address validation
    }
}

// Applied during router construction
let router = Router::builder(endpoint)
    .accept(b"/secure/1", SecureHandler)
    .incoming_filter(validation_filter)
    .spawn();

```

The filter executes synchronously on the accept loop thread, so avoid blocking operations. For complex logic, return `Accept` and perform authentication inside the async `ProtocolHandler::accept` method instead.

## Graceful Shutdown

The `Router::shutdown` method ensures all protocol handlers terminate cleanly without dropping active connections mid-stream. According to the implementation in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs):

1. **Cancels the accept loop** via `cancel_token.cancel()` (line 34).
2. **Awaits background task completion** for the main accept loop (lines 40–44).
3. **Signals all handlers** by calling `ProtocolMap::shutdown` concurrently (lines 95–101).
4. **Cancels pending accepts** and closes the endpoint (lines 97–106).

Invoke shutdown explicitly to prevent connection errors:

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

```

If the router is dropped without calling `shutdown`, the background task aborts immediately, potentially interrupting active transfers.

## Complete Example: Logger and Ping Server

The following example demonstrates serving two distinct protocols—one that logs connections and another that implements a request-response ping protocol—under a single endpoint with address validation filtering:

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

#[derive(Clone, Debug)]
struct Logger;
#[async_trait::async_trait]
impl ProtocolHandler for Logger {
    async fn accept(&self, conn: iroh::endpoint::Connection) -> Result<(), AcceptError> {
        println!("Connection from {}", conn.remote_id());
        Ok(())
    }
}

#[derive(Clone, Debug)]
struct Ping;
#[async_trait::async_trait]
impl ProtocolHandler for Ping {
    async fn accept(&self, conn: iroh::endpoint::Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = conn.accept_bi().await?;
        let mut buf = Vec::new();
        recv.read_to_end(1024).await?.into_bytes(&mut buf);
        send.write_all(b"pong").await?;
        send.finish()?;
        Ok(())
    }
}

fn addr_filter(incoming: &iroh::endpoint::Incoming) -> IncomingFilterOutcome {
    if incoming.remote_addr_validated() {
        IncomingFilterOutcome::Accept
    } else {
        IncomingFilterOutcome::Reject
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let ep = Endpoint::bind(presets::Minimal).await?;
    
    let router = Router::builder(ep)
        .accept(b"/example/logger/1", Logger)
        .accept(b"/example/ping/1", Ping)
        .incoming_filter(addr_filter)
        .spawn();
    
    tokio::signal::ctrl_c().await?;
    router.shutdown().await?;
    Ok(())
}

```

This configuration registers both ALPN strings on the underlying QUIC endpoint, allowing clients to select their desired protocol via the ALPN extension during connection establishment.

## Summary

- The **Router** in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) multiplexes multiple protocols over a single QUIC endpoint using ALPN routing.
- **RouterBuilder** configures handlers via `accept()` and optional `incoming_filter()` before `spawn()` starts the accept loop.
- **ProtocolHandler** implementations receive full ownership of `Connection` objects and must handle stream management and graceful closure.
- **IncomingFilter** executes before the TLS handshake, supporting `Accept`, `Retry`, `Reject`, or `Ignore` outcomes for early connection gating.
- **Graceful shutdown** via `router.shutdown().await` signals all handlers, cancels pending accepts, and closes the endpoint cleanly.

## Frequently Asked Questions

### What is the ALPN string used for in iroh Router?

The ALPN (Application-Layer Protocol Negotiation) string identifies which protocol handler should process an incoming connection. When a client connects, it proposes an ALPN during the QUIC handshake; the router matches this against registered handlers in the `ProtocolMap` (lines 42–45 of [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) and dispatches the connection accordingly.

### How does the Router handle unknown protocols?

If the client proposes an ALPN for which no handler is registered, the router silently ignores the connection (line 44 of [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)). This causes the client to time out or receive a protocol error, depending on the QUIC implementation, preventing resource exhaustion from unhandled protocols.

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

Yes. Implement the `IncomingFilter` trait and register it via `Router::builder().incoming_filter()`. This filter runs synchronously before cryptographic negotiation (lines 62–85 of [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)), allowing you to reject connections based on source address validation, rate limits, or other criteria without performing expensive TLS operations.

### How do I gracefully shut down a multi-protocol server in iroh?

Call `router.shutdown().await` to initiate a graceful shutdown sequence. This cancels the accept loop, waits for the background task to finish, invokes `shutdown` on each registered `ProtocolHandler` concurrently, and finally closes the endpoint. Dropping the router without calling `shutdown` aborts connections immediately.