Iroh Advanced Usage Examples: Building Custom Protocols on QUIC
Iroh advanced usage examples demonstrate how to implement custom networking protocols using the Endpoint and Router APIs, including echo servers, content-addressed blob transfers, and scalable gossip overlays on top of QUIC connections.
The n0-computer/iroh repository provides a Rust-first library for building peer-to-peer applications that dial peers by public key while automatically selecting the fastest path—direct, hole-punched, or relayed. These Iroh advanced usage examples cover production-ready patterns from the source code, showing how to compose protocol handlers, manage content-addressed storage, and deploy private relay infrastructure.
Implementing a Custom Echo Protocol
The echo protocol demonstrates the fundamental pattern for all Iroh networking: bind an endpoint, negotiate connections via Application-Layer Protocol Negotiation (ALPN), and handle bidirectional streams. This pattern is implemented in iroh/src/lib.rs where the Endpoint type manages the underlying QUIC engine.
Establishing Client Connections
The client side creates an Endpoint, connects to a remote address, and opens a bidirectional stream. Connections automatically handle hole-punching and relay fallback according to the implementation in iroh/src/lib.rs.
use iroh::Endpoint;
use anyhow::Result;
use tokio::io::{AsyncWriteExt, AsyncReadExt};
const ALPN: &[u8] = b"iroh-example/echo/0";
#[tokio::main]
async fn main() -> Result<()> {
// Bind a local endpoint that manages QUIC connections
let endpoint = Endpoint::bind().await?;
// Connect to a remote peer by address (e.g., "iroh://<public-key>@host:port")
let addr = "...remote endpoint address...".parse()?;
let conn = endpoint.connect(addr, ALPN).await?;
// Open a bidirectional QUIC stream
let (mut send, mut recv) = conn.open_bi().await?;
// Send payload and read echoed response
send.write_all(b"Hello, iroh!").await?;
send.finish()?; // Signal end-of-write
let mut response = Vec::new();
recv.read_to_end(&mut response).await?;
// Cleanup
conn.close(0u32.into(), b"bye!");
endpoint.close().await;
Ok(())
}
Registering Protocol Handlers on the Server
Servers implement the ProtocolHandler trait and register handlers with a Router. The router dispatches incoming connections based on ALPN identifiers, as defined in iroh/src/lib.rs.
use iroh::{Endpoint, Router};
use iroh::protocol::ProtocolHandler;
use iroh::connection::Connection;
use anyhow::Result;
use std::sync::Arc;
use tokio::io;
const ALPN: &[u8] = b"iroh-example/echo/0";
#[derive(Debug, Clone)]
struct Echo;
#[async_trait::async_trait]
impl ProtocolHandler for Echo {
async fn accept(&self, conn: Connection) -> Result<()> {
let (mut send, mut recv) = conn.accept_bi().await?;
io::copy(&mut recv, &mut send).await?;
send.finish()?;
conn.closed().await;
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = Endpoint::bind().await?;
let router = Router::builder(endpoint)
.accept(ALPN.to_vec(), Arc::new(Echo))
.spawn()
.await?;
// Router runs forever processing connections
Ok(())
}
Content-Addressed Blob Transfers with iroh-blobs
The iroh-blobs crate provides BLAKE3-hashed content addressing for large file transfers. The implementation in iroh-blobs/src/lib.rs exposes a BlobStore type that integrates with the same Endpoint API used for raw protocols.
use iroh::Endpoint;
use iroh::Router;
use iroh_blobs::BlobStore;
use anyhow::Result;
use std::path::PathBuf;
use std::sync::Arc;
const ALPN: &[u8] = b"iroh-blobs/transfer/0";
#[tokio::main]
async fn main() -> Result<()> {
// Initialize endpoint and persistent blob store
let endpoint = Endpoint::bind().await?;
let store = BlobStore::open(PathBuf::from("./my_blobs")).await?;
// Sender: Add file to store and push to peer
let cid = store.add_file("large_file.dat").await?;
let conn = endpoint.connect(remote_addr, ALPN).await?;
store.send(&conn, cid).await?;
// Receiver: Accept connections and automatically persist blobs
let router = Router::builder(endpoint)
.accept(ALPN.to_vec(), Arc::new(store.clone()))
.spawn()
.await?;
Ok(())
}
The blob store handles streaming verification during transfer, ensuring data integrity without requiring the entire file to reside in memory. For a complete runnable demonstration, see iroh-blobs/examples/transfer.rs in the repository.
Building Scalable Gossip Networks
iroh-gossip implements a scalable publish-subscribe overlay network on top of Iroh's connection layer. The high-level API in iroh-gossip/src/lib.rs abstracts peer discovery and mesh maintenance while exposing simple publish and subscribe methods.
Publishing Messages to a Topic
Topics are derived from byte strings and act as broadcast channels across the network:
use iroh::Endpoint;
use iroh_gossip::{Gossip, Topic};
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = Endpoint::bind().await?;
let gossip = Gossip::new(endpoint.clone()).await?;
// Create topic from arbitrary byte string
let topic = Topic::from(b"chat-room-1".as_ref());
// Publish messages at regular intervals
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
loop {
interval.tick().await;
let msg = format!("ping at {}", chrono::Utc::now());
gossip.publish(&topic, msg.into_bytes()).await?;
}
}
Subscribing to Topic Updates
Subscribers register callbacks that process incoming messages asynchronously:
use iroh::Endpoint;
use iroh_gossip::{Gossip, Topic, Message};
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = Endpoint::bind().await?;
let gossip = Gossip::new(endpoint.clone()).await?;
let topic = Topic::from(b"chat-room-1".as_ref());
// Register handler for specific topic
gossip.subscribe(&topic, |msg: Message| async move {
println!("Got: {}", String::from_utf8_lossy(&msg.payload));
}).await?;
// Keep application alive
futures::future::pending::<()>().await;
Ok(())
}
The gossip protocol handles peer discovery and mesh optimization automatically. Reference the full implementation in iroh-gossip/examples/pubsub.rs.
Deploying Private Relay Infrastructure
For testing or private deployments that require NAT traversal assistance, the iroh-relay crate provides a standalone relay server. The entry point in iroh-relay/src/main.rs supports TLS configuration and custom listening addresses.
cargo run -p iroh-relay -- --listen 0.0.0.0:2345 --cert ./cert.pem --key ./key.pem
The relay server handles client authentication and connection pooling as implemented in iroh-relay/src/lib.rs. Endpoints automatically fall back to configured relays when direct connections fail, requiring no code changes to the protocol handlers.
Summary
- Custom Protocols implement the
ProtocolHandlertrait and register withRouter::builderto handle ALPN-tagged connections viaaccept_bi()andopen_bi()streams. - Blob Transfer uses
BlobStorefromiroh-blobsfor BLAKE3-verified content addressing, integrating seamlessly with theEndpointconnection API. - Gossip Networks provide scalable publish-subscribe through
iroh-gossip, abstracting peer discovery and mesh maintenance behindTopichandles. - Relay Infrastructure can be self-hosted using
iroh-relayfor environments requiring NAT traversal assistance, with automatic fallback handled by theEndpointiniroh/src/lib.rs.
Frequently Asked Questions
What is the difference between Endpoint and Router in Iroh?
The Endpoint (iroh/src/lib.rs) manages the underlying QUIC engine, handles hole-punching, and establishes connections to remote peers. The Router registers protocol handlers against specific ALPN identifiers and dispatches incoming connections to the appropriate handler. You bind one Endpoint per application but may register multiple protocol handlers with a single Router.
How does Iroh handle NAT traversal without a relay?
Iroh attempts direct connection first, then uses hole punching via STUN and ICE protocols to establish paths through NATs. If these methods fail, the Endpoint automatically falls back to a configured relay server (such as the public relays or a private iroh-relay instance) to proxy traffic. This logic is implemented in the connection establishment code within iroh/src/lib.rs.
What is ALPN and why is it required for Iroh protocols?
Application-Layer Protocol Negotiation (ALPN) is a TLS extension that allows peers to negotiate which protocol to use over a connection. In Iroh, ALPN identifiers (byte strings like b"iroh-example/echo/0") let the Router dispatch incoming connections to the correct ProtocolHandler implementation. Each custom protocol must define a unique ALPN constant to avoid conflicts with other handlers.
Can Iroh protocols compose multiple crates like blobs and gossip?
Yes. Since both iroh-blobs and iroh-gossip use the same Endpoint type from iroh-base, you can initialize a single Endpoint and share it across multiple protocol handlers. Register both the blob store and gossip instance with the same Router::builder to run content-addressed storage and publish-subscribe messaging simultaneously over a single QUIC connection backbone.
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 →