How to Use ALPN for Custom Protocol Routing in iroh
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, the EndpointBuilder provides the alpns method to register these identifiers before binding.
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. This trait requires an accept method that receives a fully-negotiated Connection, along with optional on_accepting and shutdown methods for lifecycle management.
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, the RouterBuilder accumulates handlers and automatically updates the endpoint's advertised ALPN list via Endpoint::set_alpns before spawning the accept loop.
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) manages the dispatch process automatically. When a peer connects, the loop:
- Receives the raw
Incomingconnection - Completes the QUIC handshake via
incoming.accept() - Extracts the negotiated ALPN via
accepting.alpn().await - Looks up the corresponding handler in the
ProtocolMap - Forwards the
Connectionto the handler'sacceptmethod
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, the Endpoint::connect method forwards the ALPN to the TLS client configuration, ensuring the server receives the correct protocol negotiation request.
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:
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::alpnsiniroh/src/endpoint.rsto declare which protocols your server supports. - Implement ProtocolHandler: Create a struct implementing the
ProtocolHandlertrait fromiroh/src/protocol.rsto handle connection logic. - Build the Router: Use
Router::builderto 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::connectiniroh/src/socket.rsto negotiate the correct protocol. - Handle lifecycle properly: Implement
shutdownon your handler for graceful termination whenRouter::shutdownis 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.
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 →