How to Implement Custom Protocol Handlers Using ALPN in iroh
To implement custom protocol handlers using ALPN in iroh, you register byte-string ALPN identifiers with a Router using RouterBuilder::accept, which maps incoming QUIC connections to trait-based handlers that implement ProtocolHandler::accept.
iroh uses QUIC/TLS Application-Layer Protocol Negotiation (ALPN) to route incoming connections to the correct application-level protocol handler. When you implement custom protocol handlers using ALPN in iroh, you create secure, peer-to-peer endpoints that automatically negotiate and dispatch connections without managing low-level TLS or QUIC state manually. This architecture separates transport concerns from application logic, allowing you to define protocols as simple Rust traits.
Understanding ALPN Protocol Routing in iroh
iroh leverages QUIC/TLS ALPN to advertise supported protocols during the TLS handshake phase. The Endpoint builder in iroh/src/endpoint.rs configures the underlying TLS stack via Builder::bind, which internally calls static_config.create_server_config(self.alpn_protocols) to register the protocols this node supports.
When a remote peer connects, the accept loop in iroh/src/socket.rs extracts the negotiated ALPN identifier using incoming.alpn().await, looks up the corresponding handler in the ProtocolMap, and dispatches the connection to your custom implementation.
Implementing the ProtocolHandler Trait
Custom protocols implement the ProtocolHandler trait defined in iroh/src/protocol.rs. This trait requires a single asynchronous method, accept, which receives an established Connection and returns a Result<(), AcceptError>.
Defining the Handler Structure
Create a Rust struct that derives Debug and Clone, then implement ProtocolHandler for it. Here is a complete echo protocol handler that mirrors the reference implementation in iroh/examples/echo.rs:
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 a send and receive side.
let (mut send, mut recv) = connection.accept_bi().await?;
// Echo everything we receive 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(())
}
}
Handling Bidirectional Streams
Inside the accept method, the Connection object represents the established QUIC stream pair. Use accept_bi() to open bidirectional streams, then process data using standard async I/O operations. The handler runs on a dedicated Tokio task, allowing concurrent processing of multiple connections.
Registering Handlers with the Router
The Router acts as a dispatcher, mapping ALPN byte strings to your handlers via a ProtocolMap stored in the RouterBuilder.
Building the Endpoint
Configure your endpoint using presets like presets::N0 or presets::Minimal, then bind to an address:
use iroh::{Endpoint, endpoint::presets};
let endpoint = Endpoint::builder(presets::N0).bind().await?;
Mapping ALPN to Handlers
Use Router::builder(endpoint) to obtain a RouterBuilder, then call .accept(alpn, handler) where alpn is any &[u8] slice (commonly a static byte string such as b"/iroh/echo/1"). Finally, spawn the router to begin accepting connections:
use iroh::protocol::Router;
const ECHO_ALPN: &[u8] = b"/iroh/echo/1";
let router = Router::builder(endpoint)
.accept(ECHO_ALPN, Echo)
.spawn();
// The router now accepts connections that negotiate the echo ALPN.
// It runs until the process exits or `router.shutdown().await` is called.
The RouterBuilder::accept method stores the handler in a ProtocolMap and automatically updates the endpoint's ALPN list via endpoint.set_alpns(alpns) when spawn() is called.
Connecting from Clients
Clients connect by specifying the same ALPN byte string used during registration. The client endpoint does not need to register ALPNs for outgoing connections:
use iroh::{Endpoint, endpoint::presets};
let client = Endpoint::builder(presets::Minimal).bind().await?;
let server_addr = /* EndpointAddr obtained from router.endpoint().addr() */;
// Connect using the same ALPN identifier.
let conn = client.connect(server_addr, b"/iroh/echo/1").await?;
// Use the connection (e.g., open a bidirectional stream, send data, read echo).
let (mut send, mut recv) = conn.open_bi().await?;
// ...
Key Source Files and Implementation Details
The routing logic spans several critical files in the iroh repository:
iroh/src/protocol.rs— Defines theProtocolHandlertrait,DynProtocolHandler, and theProtocolMapstruct. Contains theRouterBuilder::acceptregistration API.iroh/src/endpoint.rs— ImplementsBuilder::alpnsandBuilder::bind, which configures the TLS server withstatic_config.create_server_config(self.alpn_protocols).iroh/src/socket.rs— Contains the low-level QUIC accept loop inhandle_connectionthat extracts the negotiated ALPN viaincoming.alpn().awaitand dispatches to registered handlers.iroh/examples/echo.rs— A complete, runnable example demonstrating a custom echo protocol handler wired with a router.iroh/tests/patchbay/util.rs— Test utilities includingTEST_ALPNconstants and example usage patterns.
Summary
- Implement
ProtocolHandlerfor your custom logic, defining theacceptmethod to handleConnectionobjects and manage bidirectional streams. - Register handlers with
RouterBuilder::acceptusing unique ALPN byte strings likeb"/myapp/1"to map incoming connections to your implementation. - Configure the endpoint using
Endpoint::builderwith presets, then bind to create the underlying QUIC/TLS configuration. - Automatic routing occurs when the accept loop extracts the negotiated ALPN and dispatches to your handler via the
ProtocolMap. - Reference implementations are available in
iroh/examples/echo.rsand test utilities iniroh/tests/patchbay/util.rs.
Frequently Asked Questions
What is ALPN and why does iroh use it?
Application-Layer Protocol Negotiation (ALPN) is a TLS extension that allows peers to negotiate application protocols during the handshake phase. iroh uses ALPN to route incoming QUIC connections to the correct handler without requiring additional round-trips or custom framing, enabling efficient multiplexing of different protocols over a single endpoint.
How do I choose an ALPN identifier for my custom protocol?
Choose a unique byte string that follows the convention b"/vendor/protocol/major_version", such as b"/mycompany/filesync/1". The identifier must be unique within your application to prevent collisions. Static byte strings like b"/iroh/echo/1" are commonly used in iroh examples and test utilities found in iroh/tests/patchbay/util.rs.
Can a single iroh endpoint handle multiple protocols simultaneously?
Yes. The Router supports multiple handlers through repeated calls to RouterBuilder::accept with different ALPN identifiers. Each incoming connection is routed to the appropriate handler based on the ALPN negotiated during the TLS handshake, allowing a single node to serve file transfers, control commands, and metrics endpoints concurrently.
Where can I find a complete working example of a custom protocol handler?
The iroh repository includes a full echo client and server implementation in iroh/examples/echo.rs. This file demonstrates both the server-side ProtocolHandler implementation and the client-side connection logic, providing a complete reference for implementing custom protocol handlers using ALPN in iroh.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →