# How to Implement a Custom ProtocolHandler in iroh

> Implement a custom ProtocolHandler in iroh by creating a type that implements the ProtocolHandler trait and registering it with RouterBuilder using a unique ALPN identifier. Learn how to add your application's protocol.

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

---

**To implement a custom protocol in iroh, create a type that implements the `ProtocolHandler` trait and register it with a `RouterBuilder` using a unique ALPN identifier.**

The `iroh` crate from n0-computer/iroh routes incoming QUIC connections by **ALPN** (Application-Layer Protocol Negotiation). When you implement a custom `ProtocolHandler`, you define how your application processes bi-directional streams after the QUIC handshake completes. This pattern allows you to build peer-to-peer protocols on top of iroh’s transport layer without managing low-level networking code.

## Understanding the ProtocolHandler Architecture

At runtime, iroh maintains a dispatch table that maps **ALPN byte sequences** to handler instances. The core components live in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs):

- **`ProtocolHandler` trait** (lines 27-44): Defines the asynchronous `accept` method that receives a fully handshaked `Connection`. It also provides optional hooks for `on_accepting` (early connection handling) and `shutdown` (cleanup).
- **`RouterBuilder::accept`** (lines 84-92): Registers your handler for a specific ALPN. The builder stores handlers in an internal `ProtocolMap`.
- **`Router::spawn`** (lines 99-122): Starts the accept loop, updates the endpoint’s ALPN advertisement list, and launches the demultiplexing task that routes connections to handlers.
- **`handle_connection`** (lines 25-46): The internal function that looks up handlers by ALPN and executes the protocol logic.

When a peer connects using your ALPN, the router automatically invokes your handler’s `accept` method with the established connection.

## Steps to Implement a Custom ProtocolHandler

### 1. Define a Unique ALPN Identifier

Choose a byte slice that uniquely identifies your protocol. Use a URI-style string to avoid collisions:

```rust
const MY_ALPN: &[u8] = b"/my-app/protocol/1";

```

### 2. Create a Handler Struct

Define a struct to hold any protocol-specific state, such as connection counters or configuration:

```rust
#[derive(Debug, Clone)]
struct MyProtocolHandler {
    state: Arc<Mutex<ProtocolState>>,
}

```

### 3. Implement the ProtocolHandler Trait

Provide an async `accept` method that implements your wire protocol. This is where you accept bidirectional streams, read data, and send responses:

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

impl ProtocolHandler for MyProtocolHandler {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        // Accept a bidirectional stream (similar to TCP)
        let (mut send, mut recv) = connection.accept_bi().await?;
        
        // Your protocol logic here
        tokio::io::copy(&mut recv, &mut send).await?;
        
        // Graceful stream closure
        send.finish()?;
        connection.closed().await;
        Ok(())
    }
    
    async fn shutdown(&self) {
        // Optional: clean up resources when the router shuts down
    }
}

```

### 4. Register with the RouterBuilder

Bind your handler to the ALPN when constructing the router:

```rust
use iroh::protocol::Router;

let router = Router::builder(endpoint.clone())
    .accept(MY_ALPN, MyProtocolHandler::new())
    .spawn();

```

### 5. Spawn and Run

The `spawn` method starts the background task that listens for incoming connections and dispatches them to your handler.

## Complete Implementation Examples

### Minimal Echo Protocol

This example from the iroh test suite demonstrates a handler that echoes received bytes back to the sender:

```rust
// Located in iroh tests (conceptually similar to iroh/tests/echo implementations)
#[derive(Debug, Clone)]
struct Echo;

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

impl ProtocolHandler for Echo {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = connection.accept_bi().await?;
        
        // Echo data back to the peer
        tokio::io::copy(&mut recv, &mut send).await?;
        
        send.finish()?;
        connection.closed().await;
        Ok(())
    }
}

// Registration:
let router = Router::builder(endpoint.clone())
    .accept(ECHO_ALPN, Echo)
    .spawn();

```

### Production Handler with State Management

For real applications, you typically need shared state and graceful shutdown handling:

```rust
use std::sync::{Arc, Mutex};
use iroh::{
    endpoint::{self, presets, Connection},
    protocol::{AcceptError, ProtocolHandler, Router},
    Endpoint,
};

#[derive(Debug, Default)]
struct TimestampProtocol {
    request_count: Arc<Mutex<u64>>,
}

const TIMESTAMP_ALPN: &[u8] = b"/my-app/timestamp/1";

impl ProtocolHandler for TimestampProtocol {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = conn.accept_bi().await?;
        
        // Read incoming data
        let mut buf = vec![0u8; 1024];
        let n = recv.read(&mut buf).await?;
        
        // Update state
        let mut counter = self.request_count.lock().unwrap();
        *counter += 1;
        
        // Send timestamped response
        let reply = format!("Request #{} received at {:?}", *counter, std::time::SystemTime::now());
        send.write_all(reply.as_bytes()).await?;
        send.finish()?;
        
        conn.closed().await;
        Ok(())
    }
    
    async fn shutdown(&self) {
        let count = *self.request_count.lock().unwrap();
        eprintln!("TimestampProtocol shutting down. Total requests: {}", count);
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Bind endpoint using minimal preset
    let endpoint = Endpoint::bind(presets::Minimal).await?;
    
    // Configure router with our handler
    let router = Router::builder(endpoint.clone())
        .accept(TIMESTAMP_ALPN, TimestampProtocol::default())
        .spawn();
    
    println!("Listening on {}", endpoint.addr());
    
    // Keep alive until Ctrl-C
    tokio::signal::ctrl_c().await?;
    router.shutdown().await?;
    Ok(());
}

```

### Connecting from a Peer Client

To connect to your custom protocol from another iroh node:

```rust
let remote_endpoint = Endpoint::bind(presets::Minimal).await?;
let conn = remote_endpoint.connect(router.endpoint().addr(), MY_ALPN).await?;
let (mut send, mut recv) = conn.open_bi().await?;

send.write_all(b"Hello").await?;
send.finish()?;

```

The peer specifies the same ALPN byte sequence; iroh negotiates the protocol during the QUIC handshake and routes the connection to your handler’s `accept` method.

## Key Source Files in n0-computer/iroh

- **[`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)**: Contains the `ProtocolHandler` trait definition (lines 27-44), `RouterBuilder` implementation with the `accept` registration method (lines 84-92), and the `Router::spawn` accept loop (lines 99-122).
- **[`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)**: Defines the `Endpoint` struct and low-level QUIC connection handling used by protocol handlers.
- **`iroh/tests/patchbay/*.rs`**: Integration tests demonstrating various protocol handler implementations, including echo servers and multi-protocol routers.
- **[`iroh/README.md`](https://github.com/n0-computer/iroh/blob/main/iroh/README.md)**: High-level overview of iroh’s architecture and quick-start examples.

## Summary

- **ALPN drives dispatch**: iroh uses Application-Layer Protocol Negotiation to route incoming QUIC connections to the appropriate handler.
- **Implement `ProtocolHandler`**: Provide an async `accept` method to handle bi-directional streams; optionally implement `on_accepting` for early connection control and `shutdown` for cleanup.
- **Register with `RouterBuilder`**: Use `.accept(alpn, handler)` to bind your implementation to a specific protocol identifier.
- **Manage state safely**: Wrap shared state in `Arc<Mutex<_>>` or similar structures since handlers must be `Clone` and may handle multiple concurrent connections.
- **Reference the source**: The trait definition and routing logic are located in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) lines 27-122.

## Frequently Asked Questions

### What is ALPN and why does iroh use it for ProtocolHandler dispatch?

**ALPN (Application-Layer Protocol Negotiation)** is a TLS extension that allows peers to negotiate which protocol to use during the initial handshake. According to the iroh source code in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), iroh registers ALPN identifiers with the QUIC endpoint; when a remote peer connects using a matching ALPN, the router automatically dispatches the connection to your registered `ProtocolHandler`. This eliminates manual protocol detection and allows a single iroh node to host multiple protocols simultaneously on different ALPN identifiers.

### Can a single iroh node handle multiple protocols at once?

Yes. A `RouterBuilder` accepts multiple `.accept()` calls with different ALPN byte sequences. As implemented in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) lines 84-92, the builder stores each handler in a `ProtocolMap`. When `Router::spawn` runs (lines 99-122), it updates the endpoint’s ALPN list to advertise all registered protocols. Incoming connections are demultiplexed based on the ALPN negotiated during the QUIC handshake.

### How do I handle graceful shutdown in my ProtocolHandler?

Implement the optional `shutdown` method on the `ProtocolHandler` trait. This async method is invoked when you call `router.shutdown().await`. Use it to close persistent connections, flush buffers, or persist state to disk. The default implementation in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) is empty, so you only need to override it if your protocol requires cleanup logic.

### What is the difference between `accept` and `on_accepting` in the ProtocolHandler trait?

**`accept`** is the primary method that receives a fully established `Connection` after the QUIC handshake completes; implement your main protocol logic here. **`on_accepting`** is an optional early hook that runs before the handshake finishes (as shown in the trait definition at lines 27-44 in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)). It is useful for 0-RTT scenarios or making early reject decisions based on the connecting peer’s address before committing resources to the connection.