# How to Implement Custom Protocol Handlers with ALPN in iroh

> Learn to implement custom protocol handlers in iroh using ALPN. Route QUIC connections effectively by registering your handler with RouterBuilder accept. Maximize performance and control.

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

---

**Implement the `ProtocolHandler` trait and register it with `RouterBuilder::accept(alpn, handler)` to route incoming QUIC connections based on the negotiated Application-Layer Protocol Negotiation (ALPN) identifier.**

iroh uses QUIC/TLS Application-Layer Protocol Negotiation (ALPN) to enable secure peer-to-peer protocol multiplexing without requiring manual TLS configuration. By implementing custom protocol handlers with ALPN in iroh, you define application-level behaviors that activate automatically when remote peers negotiate specific protocol identifiers, with the routing logic handled entirely within the library's accept loop.

## Understanding ALPN Routing Architecture

iroh routes incoming connections through a three-layer architecture that separates transport configuration from application logic.

The **endpoint** ([`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)) manages the underlying QUIC/TLS transport and advertises supported protocols via `Builder::alpns`. When you call `Builder::bind`, the code executes `static_config.create_server_config(self.alpn_protocols)` to configure the server TLS certificate with your ALPN identifiers.

The **router** ([`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) maintains a `ProtocolMap` that maps raw ALPN byte strings to boxed trait objects. The `RouterBuilder::accept` method populates this map, and `RouterBuilder::spawn` initiates the accept loop.

The **accept loop** ([`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)) runs in `handle_connection`, extracting the negotiated ALPN via `incoming.alpn().await` and dispatching to the matching handler.

## Implementing the ProtocolHandler Trait

Create a struct that implements `ProtocolHandler`, defining the `accept` method to receive a `Connection` and manage bi-directional streams.

In [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the trait requires:

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

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

impl ProtocolHandler for Echo {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        // Split the QUIC connection into send and receive sides.
        let (mut send, mut recv) = connection.accept_bi().await?;
        
        // Echo everything received back to the peer.
        tokio::io::copy(&mut recv, &mut send).await?;
        
        // Gracefully finish the send side and wait for the peer to close.
        send.finish()?;
        connection.closed().await;
        Ok(())
    }
}

```

The `accept` method runs on a dedicated Tokio task, giving you full ownership of the connection lifecycle. Open streams with `accept_bi()` (incoming) or `open_bi()` (outgoing), then close gracefully with `send.finish()` or abruptly by dropping the connection.

## Registering Handlers with the Router

Use `Router::builder` to construct a protocol router, registering handlers before spawning the accept loop.

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

#[tokio::main]
async fn main() -> n0_error::Result<()> {
    // Build an endpoint with a preset (e.g., N0) and bind it.
    let endpoint = Endpoint::builder(presets::N0).bind().await?;

    // Define a unique ALPN identifier for your protocol.
    const ECHO_ALPN: &[u8] = b"/iroh/echo/1";

    // Build the router, register the handler, and spawn the accept loop.
    let router = Router::builder(endpoint)
        .accept(ECHO_ALPN, Echo)
        .spawn();

    // The router now accepts connections negotiating the echo ALPN.
    // It runs until process exit or `router.shutdown().await`.
    Ok(())
}

```

When you call `spawn()`, the router automatically invokes `endpoint.set_alpns(alpns)` to update the endpoint's advertised protocols. The `ProtocolMap` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) stores your handler as a `DynProtocolHandler`, enabling type-erased dispatch for multiple protocols on a single endpoint.

## Configuring Endpoint ALPN Advertisements

The endpoint builder in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) exposes `Builder::alpns` to statically configure supported protocols, though this is typically handled automatically when using the router pattern.

If building custom infrastructure without the router, you must manually set the ALPN list:

```rust
let endpoint = Endpoint::builder(presets::N0)
    .alpns(vec![b"/custom/1".to_vec()])
    .bind()
    .await?;

```

The `bind` method constructs the TLS server configuration via `static_config.create_server_config(self.alpn_protocols)`, ensuring your ALPN identifiers appear in the TLS handshake.

## Connecting from the Client Side

Clients connect by specifying the target ALPN in the `connect` method. The client endpoint does not need to advertise ALPNs for outgoing connections.

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

#[tokio::main]
async fn main() -> n0_error::Result<()> {
    // Build a client endpoint (no ALPN list required for outgoing).
    let client = Endpoint::builder(presets::Minimal).bind().await?;

    // Obtain the server address from its `router.endpoint().addr()`.
    let server_addr = /* EndpointAddr obtained elsewhere */;

    // Connect using the same ALPN identifier.
    let conn = client.connect(server_addr, b"/iroh/echo/1").await?;
    
    // Open a bidirectional stream, send data, and read the echo response.
    let (mut send, mut recv) = conn.open_bi().await?;
    // ... write to send, read from recv ...
    
    Ok(())
}

```

The `connect` method ensures the TLS handshake only succeeds if the server supports the specified ALPN, preventing protocol mismatch errors before any application data flows.

## Complete Example Reference

The repository provides a working implementation in [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs), demonstrating both server and client implementations in a single file. This example illustrates proper error handling, connection cleanup, and the complete registration flow from endpoint construction to router shutdown.

Key implementation files to review:

- **[`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)**: Defines `ProtocolHandler`, `DynProtocolHandler`, `ProtocolMap`, and the `RouterBuilder` API.
- **[`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)**: Contains `Builder::bind` and the TLS configuration logic via `create_server_config`.
- **[`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)**: Houses `handle_connection`, which extracts `incoming.alpn()` and dispatches via the `ProtocolMap`.
- **[`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs)**: Contains test constants like `TEST_ALPN` showing idiomatic ALPN string usage.

## Summary

- **Implement `ProtocolHandler`** in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) to define custom protocol logic using `Connection` and stream operations.
- **Register with `RouterBuilder::accept(alpn, handler)`** to map ALPN byte strings to your implementations.
- **Spawn the router** to automatically configure endpoint ALPN advertisements and start the accept loop in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs).
- **Connect clients** using `Endpoint::connect(addr, alpn)` with matching protocol identifiers.
- **Use unique ALPN strings** (e.g., `b"/myapp/1"`) to avoid collisions with other protocols on the same endpoint.

## Frequently Asked Questions

### What ALPN string format should I use for custom protocols?

Use byte string literals like `b"/myapp/protocol/1"` or `b"\x03my\x05proto"`. The `alpn` parameter accepts any `&[u8]`, but conventional ALPN identifiers follow the length-prefixed format or use clear text paths with version numbers. Avoid using standard TLS ALPN values like `h2` or `http/1.1` unless implementing those specific protocols.

### Can I register multiple protocol handlers on a single iroh endpoint?

Yes. The `ProtocolMap` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) supports multiple entries. Chain `.accept(alpn1, handler1).accept(alpn2, handler2)` when building the router. The endpoint will advertise all registered ALPNs in the TLS handshake, and incoming connections route to the specific handler matching the negotiated protocol.

### How does iroh handle connections when the ALPN doesn't match any registered handler?

TLS negotiation fails at the transport level before application code runs. If a client attempts to connect with an ALPN not present in the endpoint's advertised list, the TLS handshake completes without agreement (or fails immediately, depending on client behavior), and the connection never reaches the `handle_connection` dispatch logic in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs).

### What is the difference between the Endpoint and the Router in iroh?

The **Endpoint** ([`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)) manages the low-level QUIC/TLS transport, connection establishment, and network addressing. The **Router** ([`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) sits above the endpoint and implements application-level protocol dispatch. While the endpoint handles wire encryption and flow control, the router manages the `ProtocolMap` and spawns tasks to run your `ProtocolHandler::accept` implementations.