# How to Use ALPN for Custom Protocol Routing in iroh

> Learn to use ALPN for custom protocol routing in iroh. Discover how QUIC ALPN identifiers route peer connections to specific protocol handlers for extensible dispatch.

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

---

**iroh uses Application-Layer Protocol Negotiation (ALPN) identifiers over QUIC/TLS to route incoming peer connections to specific protocol handlers, enabling type-safe, extensible protocol dispatch through the `Router` and `ProtocolHandler` trait.**

The iroh networking library provides a robust framework for building peer-to-peer applications using QUIC as its transport layer. By leveraging ALPN for custom protocol routing in iroh, you can register multiple application protocols on a single endpoint, allowing the network layer to automatically dispatch connections to the appropriate handler based on the negotiated protocol identifier.

## Understanding ALPN in iroh's QUIC Architecture

iroh builds its peer-to-peer networking stack directly on top of QUIC and TLS. During the TLS handshake, the client and server negotiate an ALPN identifier—a byte string that declares which application protocol they intend to use. iroh intercepts this negotiation to route connections without manual parsing or secondary handshakes.

The architecture separates concerns cleanly: the `Endpoint` manages the transport layer and advertises supported ALPNs, while the `Router` owns the dispatch logic that maps negotiated ALPN strings to your custom `ProtocolHandler` implementations.

## Configuring ALPN Protocols on the Server

### Advertising Supported Protocols with EndpointBuilder

When constructing an `Endpoint`, you must declare which ALPN identifiers your server accepts. In [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), the `EndpointBuilder` provides the `alpns` method to register these identifiers before binding.

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

let endpoint = Endpoint::builder(presets::Minimal)
    .alpns(vec![b"/my/custom/1".to_vec(), b"/my/custom/2".to_vec()])
    .bind()
    .await?;

```

The `alpns` method takes a vector of byte vectors, where each inner vector represents a unique protocol identifier. These identifiers are transmitted to connecting peers during the TLS handshake, informing them which protocols the endpoint supports.

## Implementing Custom Protocol Handlers

### The ProtocolHandler Trait

To handle incoming connections, implement the `ProtocolHandler` trait defined in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs). This trait requires an `accept` method that receives a fully-negotiated `Connection`, along with optional `on_accepting` and `shutdown` methods for lifecycle management.

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

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

impl ProtocolHandler for MyProtocol {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        // Handle the established connection here
        let (mut send, mut recv) = connection.accept_bi().await?;
        // Protocol logic...
        Ok(())
    }
}

```

The `accept` method receives ownership of the `Connection` object, giving you full access to QUIC streams and datagrams for your custom protocol implementation.

### Creating the Router

The `Router` binds your protocol handlers to specific ALPN strings. In [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the `RouterBuilder` accumulates handlers and automatically updates the endpoint's advertised ALPN list via `Endpoint::set_alpns` before spawning the accept loop.

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

let router = Router::builder(endpoint)
    .accept(b"/my/custom/1", MyProtocol)
    .accept(b"/my/custom/2", AnotherProtocol)
    .spawn();

```

Each call to `RouterBuilder::accept` registers a handler for a specific ALPN byte string. The router maintains an internal `ProtocolMap` that maps these identifiers to your handler instances.

## Handling Incoming Connections

The router's accept loop (`run_loop_fut` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)) manages the dispatch process automatically. When a peer connects, the loop:

1. Receives the raw `Incoming` connection
2. Completes the QUIC handshake via `incoming.accept()`
3. Extracts the negotiated ALPN via `accepting.alpn().await`
4. Looks up the corresponding handler in the `ProtocolMap`
5. Forwards the `Connection` to the handler's `accept` method

If the ALPN received does not match any registered handler, the router logs a warning and closes the connection gracefully.

## Connecting from the Client Side

To initiate a connection using a specific protocol, specify the ALPN identifier when calling `connect`. In [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs), the `Endpoint::connect` method forwards the ALPN to the TLS client configuration, ensuring the server receives the correct protocol negotiation request.

```rust
let conn = client_endpoint.connect(remote_addr, b"/my/custom/1").await?;

```

The client must specify an ALPN that the server has advertised; otherwise, the TLS handshake will fail with a protocol negotiation error.

## Complete Working Example

This complete example demonstrates a custom print protocol that echoes received data back to the client:

```rust
use iroh::{
    endpoint::{presets, Builder as EndpointBuilder},
    protocol::{Router, ProtocolHandler, AcceptError},
    Endpoint, Connection,
};

/// Custom protocol that prints received data and echoes a response.
#[derive(Debug, Clone)]
struct PrintProtocol;

impl ProtocolHandler for PrintProtocol {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = conn.accept_bi().await?;
        let mut buf = vec![];
        recv.read_to_end(&mut buf).await?;
        println!("Received: {}", String::from_utf8_lossy(&buf));
        
        // Echo back a response
        send.write_all(b"ok").await?;
        send.finish()?;
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Server side: Configure endpoint with ALPN and router
    let srv_ep = Endpoint::builder(presets::Minimal)
        .alpns(vec![b"/my/print/1".to_vec()])
        .bind()
        .await?;

    let router = Router::builder(srv_ep.clone())
        .accept(b"/my/print/1", PrintProtocol)
        .spawn();

    // Client side: Connect with specific ALPN
    let cli_ep = Endpoint::bind(presets::Minimal).await?;
    let remote = srv_ep.addr();
    let conn = cli_ep.connect(remote, b"/my/print/1").await?;

    // Send message using bidirectional stream
    let (mut send, mut recv) = conn.open_bi().await?;
    send.write_all(b"hello iroh").await?;
    send.finish()?;
    let mut resp = vec![];
    recv.read_to_end(&mut resp).await?;
    println!("Server replied: {}", String::from_utf8_lossy(&resp));

    // Graceful shutdown
    router.shutdown().await?;
    cli_ep.close().await;
    Ok(())
}

```

## Summary

- **Register ALPNs on the endpoint**: Use `EndpointBuilder::alpns` in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) to declare which protocols your server supports.
- **Implement ProtocolHandler**: Create a struct implementing the `ProtocolHandler` trait from [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) to handle connection logic.
- **Build the Router**: Use `Router::builder` to map ALPN strings to handlers; the router automatically manages the accept loop and dispatch.
- **Specify ALPN on connect**: Clients must provide the target ALPN to `Endpoint::connect` in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) to negotiate the correct protocol.
- **Handle lifecycle properly**: Implement `shutdown` on your handler for graceful termination when `Router::shutdown` is called.

## Frequently Asked Questions

### What is ALPN and why does iroh use it?

ALPN (Application-Layer Protocol Negotiation) is a TLS extension that allows clients and servers to negotiate which application protocol to use during the handshake. iroh uses ALPN to enable multiple protocols to coexist on a single endpoint, routing connections to the appropriate handler based on the negotiated identifier without requiring additional round trips or manual protocol selection.

### How do I register multiple protocols on one endpoint?

Register multiple protocols by calling `RouterBuilder::accept` multiple times with different ALPN byte strings. Each `accept` call maps a unique ALPN to its corresponding `ProtocolHandler` implementation. The router automatically aggregates all registered ALPNs and updates the endpoint's advertised list before starting the accept loop.

### Can I change ALPNs after the endpoint is bound?

No, once the endpoint is bound, the set of advertised ALPNs is fixed for the lifetime of the TLS configuration. However, you can dynamically change which handlers are active by shutting down the router and creating a new one with `Router::builder`, passing the same endpoint but with different handler registrations. The `Endpoint::set_alpns` method (called internally by the router) only succeeds before the endpoint begins accepting connections.

### How does protocol shutdown work in iroh?

When you call `Router::shutdown`, the router signals all registered protocol handlers to terminate. The `ProtocolHandler` trait includes an optional `shutdown` method for custom cleanup logic. Pending `accept` futures are cancelled, and the underlying endpoint closes gracefully, ensuring that any in-flight connections complete or terminate cleanly according to QUIC connection semantics.