How to Integrate iroh with Other Applications: Endpoint and Builder API Guide

Integrate iroh into any Rust application by creating an Endpoint through the Builder API, which handles cryptographic identity, NAT traversal, and QUIC connections, then use the Router to dispatch incoming streams by ALPN protocol.

The n0-computer/iroh repository provides a Rust library that embeds a complete peer-to-peer networking stack into your program. To integrate iroh with other applications, you instantiate an Endpoint—the core abstraction that manages cryptographic keys, relay connections, and address publication—then bind it to handle inbound and outbound QUIC streams. This architecture allows any existing codebase to gain direct connectivity without managing low-level socket operations or NAT traversal logic manually.

Understanding the iroh Integration Architecture

The integration surface centers on a few high-level types that encapsulate the complexity of QUIC networking.

The Endpoint as the Core Gateway

The Endpoint struct, defined in [iroh/src/endpoint.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), serves as the public API for creating connections. It owns a cryptographic identity, handles NAT traversal automatically, communicates with relay servers when direct paths are unavailable, and exposes methods for dialing and accepting connections. When you bind an endpoint, it spawns the underlying QUIC server, contacts configured relays, and publishes your address to the chosen lookup services.

Builder Pattern for Configuration

Configuration occurs through the Builder, implemented in the same file starting around line 22. The builder accepts settings for ALPN protocols, relay modes, address-lookup services (such as Pkarr DNS), and custom transport layers. Key methods include alpns(), relay_mode(), and address_lookup(). Calling bind().await on the builder finalizes the configuration and returns a ready-to-use Endpoint.

Router for Protocol Dispatch

The optional Router utility, created via Router::builder(endpoint.clone()), dispatches inbound streams to protocol handlers based on the ALPN identifier. This decouples connection management from application logic, allowing you to register multiple protocol handlers on a single endpoint.

Step-by-Step Integration Workflow

Follow this sequence to embed iroh into your existing application:

  1. Add the dependency to your Cargo.toml (iroh = "…").
  2. Create a builder using a preset like presets::N0 for sensible defaults.
  3. Configure ALPN protocols, relay mode, and address-lookup services.
  4. Bind the endpoint with builder.bind().await to initialize the networking stack.
  5. Accept inbound connections manually via endpoint.accept() or automatically via a Router.
  6. Dial remote endpoints using endpoint.connect() or connect_with_opts() for advanced scenarios.

All networking is end-to-end encrypted, requiring no external trust anchors beyond optional CA TLS configuration for relay or DNS services.

Code Examples for iroh Integration

Basic Echo Server and Client

This example demonstrates a complete server that accepts connections and a client that dials it, using the Router to handle protocol dispatch.

use iroh::{
    Endpoint,
    endpoint::{presets, Connection, Router},
    protocol::{AcceptError, ProtocolHandler},
};

const ALPN: &[u8] = b"iroh-example/echo/0";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // ---------- Server side ----------
    let server_ep = Endpoint::builder(presets::N0)
        .alpns(vec![ALPN.to_vec()])      // Register the ALPN we will accept
        .bind()
        .await?;                         // Bind (creates sockets, contacts relay)

    let router = Router::builder(server_ep.clone())
        .accept(ALPN, Echo)               // Route incoming streams to the handler
        .spawn();

    // Make the server reachable (optional, for NAT traversal)
    server_ep.online().await;

    // ---------- Client side ----------
    let client_ep = Endpoint::builder(presets::N0).bind().await?;
    let remote_addr = server_ep.addr();   // Publishable address of the server

    // Dial the server using the same ALPN
    let conn = client_ep.connect(remote_addr, ALPN).await?;
    let (mut send, mut recv) = conn.open_bi().await?;
    send.write_all(b"Hello, iroh!").await?;
    send.finish()?;
    let reply = recv.read_to_end(1024).await?;
    assert_eq!(&reply, b"Hello, iroh!");

    // Clean shutdown
    router.shutdown().await?;
    client_ep.close().await;
    server_ep.close().await;
    Ok(())
}

// Simple echo protocol handler
#[derive(Clone)]
struct Echo;
impl ProtocolHandler for Echo {
    async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = conn.accept_bi().await?;
        tokio::io::copy(&mut recv, &mut send).await?;
        send.finish()?;
        Ok(())
    }
}

Key points: presets::N0 pre-configures the Pkarr DNS lookup and a default relay set. The alpns method registers the protocol identifier, which must match on both sides. The Router::builder(...).accept(ALPN, Echo) call automatically spawns a task that invokes accept on each incoming stream.

Configuring Custom Relays and Addresses

To integrate with a private relay infrastructure or advertise specific addresses, use RelayMode::Custom and add_external_addr:

use iroh::{
    Endpoint,
    endpoint::{presets, RelayMode},
    iroh_relay::RelayConfig,
    iroh_base::RelayUrl,
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Build a custom relay map (replace with real URLs)
    let custom_relay = RelayUrl::parse("https://my-relay.example.com/")?;
    let relay_cfg = RelayConfig::default(); // configure auth, etc. if needed

    let ep = Endpoint::builder(presets::N0)
        .relay_mode(RelayMode::Custom(vec![(custom_relay.clone(), relay_cfg)]))
        .bind()
        .await?;

    // Optionally add a directly reachable address
    ep.add_external_addr("203.0.113.42:4000".parse()?).await;

    // Use the endpoint as usual
    // …
    Ok(())
}

RelayMode::Custom overrides the default relay set, directing the endpoint to your own relay cluster defined in iroh-base/src/relay_url.rs.

Implementing Custom Transport Layers

For advanced integration requiring custom packet transport, enable the unstable feature and implement the CustomTransport trait:


# Cargo.toml

iroh = { version = "…", features = ["unstable-custom-transports"] }
use std::{sync::Arc, net::SocketAddr};
use iroh::{
    Endpoint,
    endpoint::{presets, transports::CustomTransport},
    socket::transports::TransportConfig,
};

struct MyTransport;
impl CustomTransport for MyTransport {
    // Implement required methods (see iroh/src/socket/transports/custom.rs)
    // – this is an advanced use-case.
}
let custom = Arc::new(MyTransport);

let ep = Endpoint::builder(presets::N0)
    .add_custom_transport(custom)               // Register the transport
    .bind()
    .await?;

This API is gated behind the unstable-custom-transports feature and allows you to replace or supplement the default UDP socket implementation.

Key Source Files in the n0-computer/iroh Repository

When integrating iroh, reference these specific source locations to understand the implementation details:

Summary

  • The Endpoint is the primary interface for iroh integration, defined in iroh/src/endpoint.rs.
  • Use the Builder pattern to configure ALPN protocols, relay modes, and address lookup services before binding.
  • The Router automates inbound stream dispatch based on ALPN identifiers, decoupling protocol logic from connection management.
  • All connections are end-to-end encrypted using the endpoint's cryptographic identity, with optional trust anchors for external services.
  • Custom relays can be injected via RelayMode::Custom, and custom transports can be implemented via the unstable CustomTransport trait.

Frequently Asked Questions

What is the minimum code needed to integrate iroh into an existing Rust application?

At minimum, create an Endpoint using Endpoint::builder(presets::N0).bind().await, then use endpoint.connect() to dial peers or endpoint.accept() to receive connections. This single call establishes the QUIC-based P2P stack with default relay and DNS configuration, allowing immediate peer-to-peer communication.

How does iroh handle NAT traversal when integrating with external applications?

The Endpoint automatically manages NAT traversal through relay servers configured via RelayMode. When direct UDP connectivity fails, traffic routes through relays defined in RelayMap (see iroh-base/src/relay_url.rs), while address_lookup services publish public addresses for discovery according to the implementation in iroh/src/address_lookup.rs.

Can I use iroh with custom application protocols?

Yes, register your protocol identifier (ALPN) with Builder::alpns() and handle streams via a Router or manual accept calls on the Connection. For transport-level customization, implement the CustomTransport trait (unstable feature) as defined in the socket transport modules, allowing you to integrate iroh with specialized network hardware or simulation environments.

Where is the Connection struct defined and how do I use it for stream handling?

Connection is defined in iroh/src/endpoint.rs and represents a QUIC connection to a remote peer. Obtain it via Endpoint::connect() or Endpoint::accept(), then open bidirectional or unidirectional streams using methods like open_bi() and open_uni(). These streams provide standard async I/O traits for reading and writing data.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →