# How to Implement a Custom ProtocolHandler in Iroh: A Complete Guide

> Learn to implement a custom ProtocolHandler in Iroh. This guide covers trait implementation, router registration, and ALPN byte strings for your unique protocol. Get started today.

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

---

**To implement a custom ProtocolHandler in Iroh, create a type that implements the `ProtocolHandler` trait—specifically the `accept` method—and register it with a `Router` using a unique ALPN byte string that identifies your protocol.**

The Iroh library provides a Rust-native framework for building peer-to-peer applications over QUIC. A **custom ProtocolHandler** lets you define application logic that runs when remote peers connect using a specific Application-Layer Protocol Negotiation (ALPN) identifier. This article covers the complete implementation process based on the current Iroh source code.

## Understanding the ProtocolHandler Architecture

Iroh's protocol system decouples connection management from application logic. The architecture revolves around three core components: the `Router`, the `ProtocolMap`, and the trait interfaces defined in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs).

### The Router and ALPN Routing

The `Router` owns an `Endpoint` and maintains a `ProtocolMap` that associates ALPN byte strings with boxed protocol handlers. When a peer connects, the router's accept loop (`handle_connection` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) validates the ALPN, looks up the corresponding handler, and dispatches the connection to your implementation.

The `RouterBuilder` configures this mapping via the `.accept(alpn, handler)` method before spawning the router with `.spawn()`. Each ALPN must be unique across the application to prevent routing conflicts.

### The ProtocolHandler Trait Interface

The `ProtocolHandler` trait in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) defines the contract between your application code and Iroh's connection handler. The trait requires `Send + Sync + 'static` bounds and a `Debug` implementation. The core methods are:

- **`accept`** – The mandatory method that handles the connection after it is fully established.
- **`on_accepting`** – Optional hook called during the 0-RTT or early data phase before full acceptance.
- **`shutdown`** – Optional cleanup method invoked during router shutdown.

Internally, Iroh uses `DynProtocolHandler`, a dyn-compatible version that returns boxed futures, allowing the router to store diverse handler types as trait objects in the `ProtocolMap`.

## Implementing the ProtocolHandler Trait

Creating a custom handler requires defining a struct that implements the trait's required methods. The implementation pattern follows a zero-sized struct approach for stateless protocols, or you can include fields for stateful handlers.

### Required Methods and Trait Bounds

Every handler must implement the `accept` method, which receives an established `Connection` and returns a `Result<(), AcceptError>`. The method signature is:

```rust
async fn accept(&self, connection: Connection) -> Result<(), AcceptError>;

```

The trait bounds require your type to be `Send + Sync + 'static`, ensuring thread safety across the Tokio runtime. You must also derive or implement `Debug`.

### Optional Lifecycle Hooks

You can optionally implement `on_accepting` to intercept connections during the early acceptance phase. This is useful for 0-RTT validation or rejecting connections before full handshake completion. The `shutdown` method allows you to clean up resources or notify peers when the router is stopping.

## Registering Handlers with the Router

Registration binds your handler to a specific ALPN byte string. The router uses this string to dispatch incoming connections to the correct handler.

### Building the Router

The registration flow begins with `Router::builder(endpoint)` and chains the `.accept()` method for each protocol you support. Here is the pattern from [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs):

```rust
let endpoint = Endpoint::bind(presets::N0).await?;
let router = Router::builder(endpoint)
    .accept(b"/my/custom/1", MyHandler)
    .spawn();

```

The ALPN bytes (`b"/my/custom/1"` in this example) must match exactly what connecting clients use in their `endpoint.connect()` call.

### ProtocolMap and ALPN Keys

The `ProtocolMap` stored inside the router is a `HashMap` keyed by ALPN bytes containing `Box<dyn DynProtocolHandler>`. When a connection arrives, the router extracts the ALPN from the TLS handshake and calls `protocol_map.get(&alpn)` to retrieve your handler. If no handler matches, the connection is rejected.

## Handling Connection Lifecycle

Understanding when Iroh calls your handler methods helps implement proper resource management and error handling.

### The Accept Flow

When a peer connects using your registered ALPN, the router executes the following sequence:

1. Calls `handler.on_accepting` (if implemented) to validate the incoming connection
2. Upon successful acceptance, calls `handler.accept(connection)` in a new Tokio task
3. Your handler receives the `Connection` object and can open streams using `connection.accept_bi()` or `connection.accept_uni()`

The `accept` future runs independently, allowing long-running protocols without blocking the router's accept loop.

### Graceful Shutdown

When you call `router.shutdown().await`, Iroh invokes the `shutdown` method on all registered handlers concurrently. This gives each protocol a chance to close connections cleanly before the router aborts remaining tasks and drops the endpoint. Always await `router.shutdown()` before dropping the endpoint to prevent connection leaks.

## Complete Working Example

The following example from [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs) demonstrates a minimal echo protocol that accepts bidirectional streams and echoes back any received data:

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

const ALPN: &[u8] = b"iroh-example/echo/0";

#[tokio::main]
async fn main() -> Result<()> {
    let router = start_accept_side().await?;
    router.endpoint().online().await;
    connect_side(router.endpoint().addr()).await?;
    router.shutdown().await.anyerr()?;
    Ok(())
}

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

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

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

async fn connect_side(addr: EndpointAddr) -> Result<()> {
    let endpoint = Endpoint::bind(presets::N0).await?;
    let conn = endpoint.connect(addr, ALPN).await?;
    let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
    send.write_all(b"Hello, world!").await.anyerr()?;
    send.finish().anyerr()?;
    let response = recv.read_to_end(1000).await.anyerr()?;
    assert_eq!(&response, b"Hello, world!");
    conn.close(0u32.into(), b"bye!");
    endpoint.close().await;
    Ok(())
}

```

This example shows the complete lifecycle: handler definition, router registration, connection acceptance with bidirectional streams, and graceful shutdown.

## Summary

- **Implement `ProtocolHandler`** – Define a type with `Debug` that implements the `accept` method to handle incoming connections.
- **Register with ALPN** – Use `Router::builder(endpoint).accept(alpn, handler)` to bind your handler to a specific protocol identifier.
- **Handle lifecycle** – Optionally implement `on_accepting` for early validation and `shutdown` for cleanup; always await `router.shutdown()` for clean termination.
- **Thread safety** – Ensure your handler meets `Send + Sync + 'static` bounds as required by the trait definition in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs).

## Frequently Asked Questions

### What is the ALPN string used for in Iroh?

The ALPN (Application-Layer Protocol Negotiation) byte string identifies which protocol a peer wants to use when connecting. In Iroh, the `Router` inspects the ALPN from the TLS handshake and dispatches the connection to the corresponding `ProtocolHandler`. Both client and server must agree on the same ALPN bytes for the connection to succeed.

### Do I need to implement on_accepting and shutdown?

No. The `ProtocolHandler` trait provides default implementations for `on_accepting` and `shutdown`. You only need to implement `on_accepting` if you require early connection validation or 0-RTT handling. Implement `shutdown` if your protocol needs to clean up resources or notify peers before the router closes. The only mandatory method is `accept`.

### Can I use custom transports with ProtocolHandler?

Yes. The `ProtocolHandler` trait is transport-agnostic. As shown in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs), you can use custom transports with your handler by configuring the `Endpoint` with a custom transport before passing it to `Router::builder`. The handler itself works with the `Connection` abstraction regardless of the underlying transport.

### How does Iroh handle multiple protocols?

Iroh supports multiple protocols simultaneously by storing each registration in a `ProtocolMap`. You can chain multiple `.accept()` calls on the `RouterBuilder`, each with a unique ALPN and handler. The router listens on a single endpoint and automatically routes connections to the appropriate handler based on the ALPN string provided by the connecting peer.