Building Multi-Protocol Servers with Router in iroh
The iroh Router allows a single QUIC Endpoint to accept and route connections for multiple protocols simultaneously using ALPN identifiers, with support for early connection filtering and graceful shutdown.
The n0-computer/iroh repository provides a Rust networking stack designed for peer-to-peer applications. Building multi-protocol servers with Router in iroh enables developers to multiplex different application protocols over a single QUIC endpoint, reducing operational complexity while maintaining clean separation of concerns between protocol implementations.
Router Architecture and Core Components
The router implementation resides primarily in iroh/src/protocol.rs, where the core types coordinate to manage connection lifecycle and protocol dispatch.
RouterBuilder and Protocol Registration
The RouterBuilder serves as the configuration stage before the server begins accepting connections. According to the source code in iroh/src/protocol.rs, calling Router::builder(endpoint) returns a builder instance (lines 405–407) that accumulates protocol handlers and optional configuration.
Key methods on RouterBuilder include:
accept(alpn, handler)– Registers aProtocolHandlerfor a specific ALPN byte string, storing the mapping in an internalProtocolMap(lines 85–92).incoming_filter(filter)– Sets anIncomingFilterfunction to evaluate connections before the TLS handshake completes (lines 72–74).spawn()– Finalizes the builder, registers the complete ALPN list on the endpoint, and launches the background accept loop (lines 99–122).
ProtocolHandler Trait
Each protocol implements the ProtocolHandler trait, which defines how the router handles incoming connections for a specific ALPN. The trait requires an accept method and optionally supports on_accepting:
#[async_trait::async_trait]
impl ProtocolHandler for MyHandler {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
// Protocol-specific logic here
Ok(())
}
}
When a client connects, the router extracts the ALPN from the TLS handshake (lines 33–40) and looks up the corresponding handler in the ProtocolMap (lines 42–45). If no handler matches, the connection is silently ignored (line 44).
IncomingFilter for Early Gating
The IncomingFilter trait enables security and resource management decisions before committing to a full TLS handshake. The filter receives an Incoming connection reference and returns one of four IncomingFilterOutcome variants:
Accept– Proceed with normal handshake and protocol selection.Retry– Send a QUIC retry token to validate the client's address (incoming.retry()), then skip further processing.Reject– Actively refuse the connection with aCONNECTION_REFUSEDerror.Ignore– Drop the connection silently, causing the client to time out.
The router evaluates the filter at lines 62–85 of iroh/src/protocol.rs, allowing you to reject unvalidated addresses or implement rate limiting before cryptographic operations occur.
Creating a Multi-Protocol Server
To instantiate a router, bind an Endpoint and chain the builder methods to register your handlers:
use iroh::{
Endpoint,
endpoint::presets,
protocol::{ProtocolHandler, Router, AcceptError},
};
use n0_error::Result;
#[derive(Clone)]
struct Echo;
#[async_trait::async_trait]
impl ProtocolHandler for Echo {
async fn accept(&self, conn: iroh::endpoint::Connection) -> Result<(), AcceptError> {
let (mut send, mut recv) = conn.accept_bi().await?;
tokio::io::copy(&mut recv, &mut send).await?;
send.finish()?;
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = Endpoint::bind(presets::N0).await?;
let router = Router::builder(endpoint)
.accept(b"/echo/1", Echo)
.accept(b"/chat/1", ChatHandler) // Additional protocol
.spawn();
// Server runs until explicit shutdown
tokio::signal::ctrl_c().await?;
router.shutdown().await?;
Ok(())
}
This pattern, demonstrated in iroh/examples/echo.rs, binds a single endpoint that simultaneously serves the /echo/1 and /chat/1 protocols, routing each connection to its respective handler based on the ALPN negotiated during the QUIC handshake.
Implementing Protocol Handlers
Protocol handlers operate on Connection objects provided by the router. The handler fully owns the connection once the router delegates it, responsible for stream management and clean termination.
The echo server example in iroh/examples/echo.rs (lines 84–100) illustrates a complete implementation:
#[async_trait::async_trait]
impl ProtocolHandler for Echo {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
// Accept a bidirectional stream
let (mut send, mut recv) = connection.accept_bi().await?;
// Copy received data back to sender
tokio::io::copy(&mut recv, &mut send).await?;
send.finish()?;
// Wait for connection close to ensure clean shutdown
connection.closed().await;
Ok(())
}
}
Handlers should await connection.closed() or explicit stream closure to prevent resource leaks, as the router tracks active connections during shutdown.
Filtering Connections Before TLS
Implement IncomingFilter to reject unwanted connections before cryptographic handshaking consumes resources. This is particularly useful for mitigating denial-of-service attacks or enforcing address validation:
use iroh::protocol::{IncomingFilterOutcome, IncomingFilter};
fn validation_filter(incoming: &iroh::endpoint::Incoming) -> IncomingFilterOutcome {
if incoming.remote_addr_validated() {
IncomingFilterOutcome::Accept
} else {
IncomingFilterOutcome::Retry // Force address validation
}
}
// Applied during router construction
let router = Router::builder(endpoint)
.accept(b"/secure/1", SecureHandler)
.incoming_filter(validation_filter)
.spawn();
The filter executes synchronously on the accept loop thread, so avoid blocking operations. For complex logic, return Accept and perform authentication inside the async ProtocolHandler::accept method instead.
Graceful Shutdown
The Router::shutdown method ensures all protocol handlers terminate cleanly without dropping active connections mid-stream. According to the implementation in iroh/src/protocol.rs:
- Cancels the accept loop via
cancel_token.cancel()(line 34). - Awaits background task completion for the main accept loop (lines 40–44).
- Signals all handlers by calling
ProtocolMap::shutdownconcurrently (lines 95–101). - Cancels pending accepts and closes the endpoint (lines 97–106).
Invoke shutdown explicitly to prevent connection errors:
router.shutdown().await?;
If the router is dropped without calling shutdown, the background task aborts immediately, potentially interrupting active transfers.
Complete Example: Logger and Ping Server
The following example demonstrates serving two distinct protocols—one that logs connections and another that implements a request-response ping protocol—under a single endpoint with address validation filtering:
use iroh::{
Endpoint,
endpoint::presets,
protocol::{AcceptError, IncomingFilterOutcome, ProtocolHandler, Router},
};
use n0_error::Result;
#[derive(Clone, Debug)]
struct Logger;
#[async_trait::async_trait]
impl ProtocolHandler for Logger {
async fn accept(&self, conn: iroh::endpoint::Connection) -> Result<(), AcceptError> {
println!("Connection from {}", conn.remote_id());
Ok(())
}
}
#[derive(Clone, Debug)]
struct Ping;
#[async_trait::async_trait]
impl ProtocolHandler for Ping {
async fn accept(&self, conn: iroh::endpoint::Connection) -> Result<(), AcceptError> {
let (mut send, mut recv) = conn.accept_bi().await?;
let mut buf = Vec::new();
recv.read_to_end(1024).await?.into_bytes(&mut buf);
send.write_all(b"pong").await?;
send.finish()?;
Ok(())
}
}
fn addr_filter(incoming: &iroh::endpoint::Incoming) -> IncomingFilterOutcome {
if incoming.remote_addr_validated() {
IncomingFilterOutcome::Accept
} else {
IncomingFilterOutcome::Reject
}
}
#[tokio::main]
async fn main() -> Result<()> {
let ep = Endpoint::bind(presets::Minimal).await?;
let router = Router::builder(ep)
.accept(b"/example/logger/1", Logger)
.accept(b"/example/ping/1", Ping)
.incoming_filter(addr_filter)
.spawn();
tokio::signal::ctrl_c().await?;
router.shutdown().await?;
Ok(())
}
This configuration registers both ALPN strings on the underlying QUIC endpoint, allowing clients to select their desired protocol via the ALPN extension during connection establishment.
Summary
- The Router in
iroh/src/protocol.rsmultiplexes multiple protocols over a single QUIC endpoint using ALPN routing. - RouterBuilder configures handlers via
accept()and optionalincoming_filter()beforespawn()starts the accept loop. - ProtocolHandler implementations receive full ownership of
Connectionobjects and must handle stream management and graceful closure. - IncomingFilter executes before the TLS handshake, supporting
Accept,Retry,Reject, orIgnoreoutcomes for early connection gating. - Graceful shutdown via
router.shutdown().awaitsignals all handlers, cancels pending accepts, and closes the endpoint cleanly.
Frequently Asked Questions
What is the ALPN string used for in iroh Router?
The ALPN (Application-Layer Protocol Negotiation) string identifies which protocol handler should process an incoming connection. When a client connects, it proposes an ALPN during the QUIC handshake; the router matches this against registered handlers in the ProtocolMap (lines 42–45 of iroh/src/protocol.rs) and dispatches the connection accordingly.
How does the Router handle unknown protocols?
If the client proposes an ALPN for which no handler is registered, the router silently ignores the connection (line 44 of iroh/src/protocol.rs). This causes the client to time out or receive a protocol error, depending on the QUIC implementation, preventing resource exhaustion from unhandled protocols.
Can I filter connections before the TLS handshake completes?
Yes. Implement the IncomingFilter trait and register it via Router::builder().incoming_filter(). This filter runs synchronously before cryptographic negotiation (lines 62–85 of iroh/src/protocol.rs), allowing you to reject connections based on source address validation, rate limits, or other criteria without performing expensive TLS operations.
How do I gracefully shut down a multi-protocol server in iroh?
Call router.shutdown().await to initiate a graceful shutdown sequence. This cancels the accept loop, waits for the background task to finish, invokes shutdown on each registered ProtocolHandler concurrently, and finally closes the endpoint. Dropping the router without calling shutdown aborts connections immediately.
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 →