# How to Implement Custom Application Protocol Handlers in iroh

> Learn to implement custom application protocol handlers in iroh by creating a Rust type that implements the ProtocolHandler trait and registering it with the router.

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

---

**Custom protocol handlers in iroh are implemented by creating a Rust type that implements the `ProtocolHandler` trait and registering it with the router via `RouterBuilder::accept()` using a unique ALPN identifier.**

iroh routes incoming QUIC connections using Application-Layer Protocol Negotiation (ALPN) strings, the same extensibility mechanism used by HTTP/3 and SSH. This allows you to extend the `n0-computer/iroh` networking stack with application-specific protocols. Below is a complete guide based on the actual source implementation showing how to register custom handlers and manage protocol lifecycles.

## Architecture of the Protocol Handler System

The protocol handler system consists of three core components: the trait definition, the registration API, and the connection dispatch loop.

### The ProtocolHandler Trait

At the heart of the system is the `ProtocolHandler` trait defined in `iroh/src/protocol.rs#L27-L31`. This trait defines the async callbacks the router invokes for each matching connection:

```rust
#[async_trait]
pub trait ProtocolHandler: Send + Sync + Debug + 'static {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError>;
    async fn on_accepting(&self, conn: Connecting) -> Result<Connection> { ... }
    async fn shutdown(&self) -> Result<()> { ... }
}

```

Only the `accept` method is required. The `on_accepting` hook optionally intercepts the connection during the handshake phase, and `shutdown` enables cleanup when the router terminates.

### Router Registration and ALPN Management

Handlers register with the router through `RouterBuilder::accept` located at `iroh/src/protocol.rs#L84-L92`. This method maps an ALPN byte string to your handler implementation:

```rust
impl RouterBuilder {
    pub fn accept<H: ProtocolHandler>(mut self, alpn: impl AsRef<[u8]>, handler: H) -> Self {
        let alpn = alpn.as_ref().to_vec();
        self.protocols.insert(alpn, Arc::new(handler));
        self
    }
}

```

When you call `Router::spawn()`, the router automatically invokes `Endpoint::set_alpns` (defined in `iroh/src/endpoint.rs#L49-L59`) to advertise all registered protocols to connecting peers.

### Connection Dispatch Flow

The acceptance loop in `iroh/src/protocol.rs#L124-L142` extracts the ALPN from the TLS handshake and dispatches to the matching handler:

1. The router waits for incoming QUIC connections
2. Upon connection, it reads the ALPN negotiated during the TLS handshake
3. It looks up the ALPN in the internal `ProtocolMap`
4. If found, it calls `handler.on_accepting()` (default returns the connection), then `handler.accept()`
5. If no handler matches, the connection is closed immediately

## Implementing a Custom Protocol Handler

Creating a custom protocol requires three steps: defining the handler struct, implementing the trait, and registering with the router.

### Step 1: Define the Handler Struct

Create a struct that will hold your protocol state. Handlers must be `Send + Sync` and `'static`:

```rust
use std::sync::Arc;
use iroh::protocol::ProtocolHandler;

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

```

### Step 2: Implement the ProtocolHandler Trait

Implement the required `accept` method and optionally `on_accepting` or `shutdown`. Here is the echo protocol implementation from [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs):

```rust
use iroh::endpoint::Connection;
use iroh::protocol::AcceptError;
use n0_error::Result;

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

```

### Step 3: Register with the Router

Bind an endpoint and register your handler with a unique ALPN string:

```rust
use iroh::{endpoint::{Endpoint, presets}, protocol::Router};

#[tokio::main]
async fn main() -> Result<()> {
    // Bind using production-grade defaults
    let endpoint = Endpoint::bind(presets::N0).await?;
    
    // Define a unique ALPN identifier for your protocol
    const ECHO_ALPN: &[u8] = b"/iroh/echo/1";
    
    // Build router, register handler, and spawn accept loop
    let router = Router::builder(endpoint)
        .accept(ECHO_ALPN, EchoProtocol)
        .spawn();
    
    println!("Server running at: {}", router.endpoint().addr());
    
    // Shutdown handling
    router.shutdown().await?;
    Ok(())
}

```

### Complete Example: Echo Protocol

Here is a complete, runnable example demonstrating both server and client:

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

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

#[tokio::main]
async fn main() -> Result<()> {
    // Server setup
    let endpoint = Endpoint::bind(presets::N0).await?;
    const ECHO_ALPN: &[u8] = b"/iroh/echo/1";
    
    let router = Router::builder(endpoint)
        .accept(ECHO_ALPN, Echo)
        .spawn();
    
    println!("Server address: {}", router.endpoint().addr());
    
    // Client demonstration
    let client = Endpoint::bind(presets::N0).await?;
    let conn = client.connect(router.endpoint().addr(), ECHO_ALPN).await?;
    let (mut send, mut recv) = conn.accept_bi().await?;
    
    // Send payload
    let payload = b"hello iroh!";
    send.write_all(payload).await?;
    send.finish()?;
    
    // Receive echo
    let mut echoed = Vec::new();
    tokio::io::copy(&mut recv, &mut echoed).await?;
    println!("Echoed back: {}", String::from_utf8_lossy(&echoed));
    
    // Cleanup
    router.shutdown().await?;
    client.close().await;
    Ok(())
}

```

## Key Implementation Details

### ALPN String Selection

Choose ALPN values that follow the format `/iroh/<protocol-name>/<version>` to avoid collisions with built-in protocols. The ALPN is a byte sequence, not a Unicode string, though UTF-8 is conventional.

### Thread Safety and Shared State

Handlers are wrapped in `Arc` and must be thread-safe. For shared mutable state, wrap data in `Arc<Mutex<T>>` or use atomic types. The `accept` method runs in a spawned task, so long-running operations do not block other connections.

### Graceful Shutdown Handling

Implement the optional `shutdown` method if your protocol requires cleanup. The router calls this on all registered handlers before aborting pending connections during `router.shutdown().await`:

```rust
async fn shutdown(&self) -> Result<()> {
    // Close open streams, flush buffers, etc.
    Ok(())
}

```

## Summary

- **Implement `ProtocolHandler`** by defining the `accept` method to handle incoming bi-directional streams
- **Register with `RouterBuilder::accept()`** using a unique ALPN byte string at `iroh/src/protocol.rs#L84-L92`
- **The router automatically advertises** registered ALPNs via `Endpoint::set_alpns` at `iroh/src/endpoint.rs#L49-L59`
- **Connection dispatch** occurs in the accept loop at `iroh/src/protocol.rs#L124-L142` based on TLS ALPN negotiation
- **Optional hooks** include `on_accepting` for handshake interception and `shutdown` for resource cleanup

## Frequently Asked Questions

### What is the minimum required implementation for a protocol handler?

You must implement only the `accept` method of the `ProtocolHandler` trait. This method receives a `Connection` object and returns `Result<(), AcceptError>`. The `on_accepting` and `shutdown` methods have default implementations that simply pass through the connection or return immediately, respectively.

### How does iroh route incoming connections to the correct handler?

The router extracts the ALPN protocol identifier from the TLS handshake during connection establishment. It looks up this byte string in an internal `ProtocolMap` (populated via `RouterBuilder::accept`), then dispatches the connection to the corresponding handler's `accept` method. If no handler matches the ALPN, the connection is closed.

### Can I modify the connection before the handler accepts it?

Yes. Override the `on_accepting` method in your `ProtocolHandler` implementation. This method receives a `Connecting` object and returns a `Result<Connection>`, allowing you to inspect or reject the connection before the main `accept` logic runs. The default implementation simply awaits the connection and returns it unchanged.

### How do I handle cleanup when shutting down a protocol?

Implement the optional `shutdown` method in your protocol handler. When you call `Router::shutdown()`, the router invokes this method on all registered handlers before aborting pending connections. Use this to close open streams, flush buffers, or signal background tasks to terminate gracefully.