# How to Implement a Custom Protocol Handler Using iroh's Router

> Learn how to implement a custom protocol handler with irohs Router. Discover how to register handlers and accept connections using the ProtocolHandler trait and ALPN identifiers.

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

---

**To implement a custom protocol handler in iroh, create a type that implements the `ProtocolHandler` trait, register it with a `RouterBuilder` using the `accept()` method with a unique ALPN identifier, and spawn the router to start accepting connections.**

The iroh library provides a high-performance QUIC-based networking stack for peer-to-peer applications. To add custom application logic, you implement a custom protocol handler using the `Router` API, which dispatches incoming connections based on Application-Layer Protocol Negotiation (ALPN) identifiers as defined in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs).

## Understanding the ProtocolHandler Trait

The foundation of any custom protocol is the **`ProtocolHandler`** trait defined in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) (lines 27-48). This trait defines how your application handles incoming QUIC connections.

The trait requires three primary methods:

- **`accept`** – The core method that processes established connections. It receives a `Connection` object (from [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)) and runs your protocol logic.
- **`on_accepting`** – An optional hook called before the TLS handshake completes. Use this to implement 0-RTT (zero round-trip time) acceptance or early validation.
- **`shutdown`** – An optional hook for resource cleanup when the router terminates.

When a connection arrives, the router's internal accept loop (`run_loop_fut`) looks up the ALPN in the `ProtocolMap` and calls these methods in sequence.

## Registering Your Handler with the Router

To wire your handler into the networking stack, use the **Router builder pattern**. The `Router::builder` function creates a `RouterBuilder` that collects protocol registrations before spawning the accept loop.

In [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) (lines 85-90), the builder stores your handler in a `ProtocolMap`:

```rust
let router = iroh::protocol::Router::builder(endpoint)
    .accept(MY_ALPN, MyHandler)
    .spawn();

```

Key implementation details from the source code:

- **`RouterBuilder::spawn()`** (lines 99-122) creates the background accept loop and returns a `Router` handle.
- The router automatically adds your ALPN to the endpoint's advertised protocols.
- **`Router::shutdown`** gracefully terminates all protocol handlers and closes connections.

## Minimal Implementation: Echo Protocol

Here is a complete, minimal example implementing an echo protocol. This handler accepts bidirectional streams and echoes data back to the client.

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

const MY_ECHO_ALPN: &[u8] = b"/myapp/echo/1";

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

impl ProtocolHandler for MyEcho {
    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() -> anyhow::Result<()> {
    let endpoint = iroh::endpoint::presets::N0.bind().await?;
    let router = iroh::protocol::Router::builder(endpoint)
        .accept(MY_ECHO_ALPN, MyEcho)
        .spawn();
    
    println!("Router listening – press Ctrl-C to stop");
    tokio::signal::ctrl_c().await?;
    router.shutdown().await?;
    Ok(())
}

```

This example implements only the required `accept` method. The default `on_accepting` implementation awaits the standard handshake, and the router handles all connection dispatching automatically.

## Advanced Hooks: 0-RTT and Graceful Shutdown

For protocols requiring early handshake intervention or resource management, implement the optional trait hooks.

### Implementing 0-RTT Support

Use **`on_accepting`** to accept connections before the full TLS handshake completes. This is critical for latency-sensitive protocols.

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

const MY_0RTT_ALPN: &[u8] = b"/myapp/0rtt/1";

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

impl ProtocolHandler for MyZeroRtt {
    async fn on_accepting(&self, accepting: Accepting) -> Result<Connection, AcceptError> {
        let conn = accepting.into_0rtt().await?;
        println!("0-RTT connection established");
        Ok(conn)
    }

    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = connection.accept_bi().await?;
        send.write_all(b"hello from 0-RTT").await?;
        send.finish()?;
        connection.closed().await;
        Ok(())
    }

    async fn shutdown(&self) {
        println!("MyZeroRtt cleaning up resources");
    }
}

```

The `on_accepting` hook receives an `Accepting` object and can convert it to a `Connection` immediately using `into_0rtt()`, bypassing standard handshake latency.

### Resource Cleanup

Implement **`shutdown`** to close pending connections, flush state, or signal background tasks when the router terminates via `router.shutdown().await`.

## Connection Filtering

Optionally, provide an **`incoming_filter`** to the `RouterBuilder` to reject, retry, or ignore connections based on the client's address before the ALPN is read.

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

let router = Router::builder(endpoint)
    .incoming_filter(Arc::new(|incoming| {
        if incoming.remote_addr_validated() {
            IncomingFilterOutcome::Accept
        } else {
            IncomingFilterOutcome::Retry
        }
    }))
    .accept(MY_ECHO_ALPN, MyEcho)
    .spawn();

```

The filter can return `Accept`, `Reject`, `Retry`, or `Ignore` to control connection flow. See the test suite in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) (lines 720-800) for examples of each outcome.

## Summary

- **Implement `ProtocolHandler`** in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) to define your protocol logic.
- **Register with `RouterBuilder`** using `accept(alpn, handler)` to map ALPN identifiers to your implementation.
- **Spawn the router** to start the accept loop that dispatches connections based on the ALPN.
- **Use `on_accepting`** for 0-RTT or early handshake logic, and **`shutdown`** for graceful cleanup.
- **Reference key files**: [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) for the router and trait definitions, and [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) for the `Connection` API.

## Frequently Asked Questions

### What is the ALPN identifier and why does it matter?

The ALPN (Application-Layer Protocol Negotiation) identifier is a byte string that uniquely identifies your protocol, such as `b"/myapp/echo/1"`. When a client connects, iroh uses this identifier to dispatch the connection to the correct handler registered in the `ProtocolMap`. Without a unique ALPN, the router cannot distinguish between different protocol handlers.

### How do I gracefully shut down a custom protocol handler?

Implement the `shutdown` method on your `ProtocolHandler` trait. When you call `router.shutdown().await`, the router invokes this method on all registered handlers, allowing you to close streams, flush buffers, or terminate background tasks before the QUIC connections are closed.

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

Yes. Implement the `on_accepting` method to intercept the connection during the handshake phase. Use `accepting.into_0rtt().await` to convert the `Accepting` state into a `Connection` immediately, enabling 0-RTT data transmission. This is implemented in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) as part of the `ProtocolHandler` trait hooks.

### How do I reject suspicious incoming connections?

Provide an `incoming_filter` closure to the `RouterBuilder`. This filter receives incoming connection metadata and returns an `IncomingFilterOutcome`—`Accept`, `Reject`, `Retry`, or `Ignore`—allowing you to block addresses or require QUIC retry tokens before the protocol handler is invoked.