Best Practices for Using Iroh Modules in the n0-computer/iroh Library

Start with presets::N0 to configure the Endpoint Builder, explicitly define relay modes and crypto providers, and implement proper address lookup for production peer-to-peer QUIC connections.

The n0-computer/iroh library offers a modular architecture for building peer-to-peer applications, centralizing QUIC connection management, relay handling, and address discovery behind the Endpoint type. Mastering the Builder pattern and its associated presets is critical for avoiding common misconfiguration pitfalls. These best practices for using Iroh modules will help you establish secure, performant connections while leveraging the library's full-featured transport and discovery capabilities.

Start with Presets to Eliminate Configuration Errors

The Endpoint is the central type in Iroh, and you construct it using a Builder that accepts a preset configuration. Presets are reusable bundles of sane defaults that eliminate the most common source of misconfiguration.

presets::N0 provides a fully functional endpoint with a crypto provider, DNS address lookup, and the default Number-0 relays. This is the recommended starting point for most internet-facing applications. For scenarios requiring tighter control, presets::Minimal offers a stripped-down foundation where you manually configure each component.

The preset implementation in iroh/src/endpoint/presets.rs handles conditional compilation logic for crypto providers. If you use presets::Minimal or build without a preset, you must explicitly set the crypto provider as shown in lines 58-78:

use std::sync::Arc;

builder = builder.crypto_provider(
    Arc::new(rustls::crypto::ring::default_provider())
);

Configure Relay Modes and Crypto Providers Explicitly

After selecting a preset, configure the Relay Mode to determine which relay servers your endpoint contacts. The RelayMode enum is re-exported in iroh/src/lib.rs and supports three distinct modes:

  • RelayMode::Default: Uses the default Number-0 relay infrastructure.
  • RelayMode::Disabled: Essential for LAN-only tools or environments with guaranteed direct connectivity.
  • RelayMode::Custom: Points to corporate or self-hosted relay servers using a HashMap<RelayUrl, RelaySettings>.

The conversion from RelayMode to transport occurs in Builder::relay_mode within iroh/src/endpoint.rs (lines 542-574). Always set this explicitly to document your network topology:

use iroh::endpoint::RelayMode;

let builder = Endpoint::builder(presets::N0)
    .relay_mode(RelayMode::Default);

Manage Address Discovery and External Addresses

Iroh's modular architecture includes sophisticated address discovery via iroh/src/address_lookup.rs. The default N0 DNS/PKARR service is sufficient for most scenarios, but production applications require careful management of external addressing.

Add static public IPs known ahead of time using builder.external_addr(addr). For dynamic addresses discovered at runtime, call endpoint.add_external_addr(addr).await after binding. These addresses are advertised to peers and significantly improve NAT traversal success rates.

For private networks, implement a custom DnsAddressLookup pointing at an internal DNS server:

use iroh::address_lookup::{DnsAddressLookup, AddrFilter};

let custom_dns = DnsAddressLookup::new("my.internal.dns".into())
    .with_filter(AddrFilter::allow_ipv4());

Control which addresses are published using builder.addr_filter(filter) to prune sensitive IPs or relay URLs from public view.

Optimize Transport Configuration and Port Mapping

The transport layer in iroh/src/socket/transports.rs handles low-level IP and relay transports. When customizing, start with a clean slate using builder.clear_ip_transports() before adding specific bind addresses:

let endpoint = Endpoint::builder(presets::Minimal)
    .clear_ip_transports()
    .bind_addr_with_opts(
        "192.0.2.10:0",
        iroh::endpoint::BindOpts::default()
            .set_prefix_len(24)
            .set_is_default_route(true),
    )?;

The Portmapper (iroh/src/portmapper.rs) provides optional UPnP/PCP/NAT-PMP for automatic gateway configuration. Enable this only with user consent, as UPnP can unintentionally expose services. In containerized or restrictive environments, explicitly disable it:

use iroh::portmapper::PortmapperConfig;

builder.portmapper_config(PortmapperConfig::Disabled);

Configure TLS and Application-Level Protocol Negotiation

TLS configuration in iroh/src/tls.rs provides authentication and encryption for QUIC streams and HTTPS calls to relays. When not using presets, you must provide a crypto provider via builder.crypto_provider().

The ALPN (Application-Layer Protocol Negotiation) vector identifies your protocol on the QUIC connection. Always set at least one ALPN on the builder or on an already-bound endpoint using set_alpns. Avoid generic strings that may clash with other services:

let endpoint = Endpoint::builder(presets::N0)
    .alpns(vec![b"my-app/1.0".to_vec()])
    .bind()
    .await?;

Leverage Hooks for Observability and Policy

The hooks system in iroh/src/endpoint/hooks.rs allows you to inject logic at specific lifecycle stages. Implement EndpointHooks to audit connections or enforce policy during the handshake:

use iroh::endpoint::hooks::{EndpointHooks, AfterHandshakeOutcome};

struct AuditHook;

impl EndpointHooks for AuditHook {
    async fn after_handshake(&self, remote: iroh::EndpointId) -> AfterHandshakeOutcome {
        tracing::info!("Handshake completed with {}", remote.fmt_short());
        AfterHandshakeOutcome::Accept
    }
}

let ep = Endpoint::builder(presets::N0)
    .hooks(AuditHook)
    .bind()
    .await?;

Utilize Net Report for Diagnostics

The net report component in iroh/src/net_report/mod.rs probes relays to discover NAT behavior, captive portals, and latency. While this feature is unstable, it provides invaluable diagnostics during development. Configure it via the builder:

builder.net_report_config(NetReportConfig::default())

Access runtime metrics programmatically using endpoint.net_report() when available.

Ensure Graceful Shutdown and Resource Cleanup

Always call endpoint.close().await before dropping the last Endpoint handle. This ensures the underlying QUIC socket terminates cleanly and stops all background tasks including address lookup, portmapper, and net report services.

Avoid blocking calls in async contexts. All Iroh API methods are async and require the Tokio runtime, which the crate depends on. Never call .await inside std::thread::spawn without a proper runtime context.

Practical Implementation Examples

Simple Client-Server with N0 Preset

This example demonstrates bi-directional stream exchange using the recommended preset and ALPN configuration:

use iroh::{Endpoint, endpoint::presets};
use n0_error::Result;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<()> {
    // Server side
    let server = Endpoint::builder(presets::N0)
        .alpns(vec![b"my-proto".to_vec()])
        .bind()
        .await?;

    tokio::spawn(async move {
        let conn = server.accept().await?.await?;
        let (mut send, mut recv) = conn.accept_bi().await?;
        let mut buf = vec![0; 5];
        recv.read_exact(&mut buf).await?;
        println!("Server received: {:?}", std::str::from_utf8(&buf));
        send.write_all(b"world").await?;
        send.finish().await?;
        Ok::<_, n0_error::Error>(())
    });

    // Client side
    let client = Endpoint::builder(presets::N0)
        .alpns(vec![b"my-proto".to_vec()])
        .bind()
        .await?;

    let server_addr = client.addr();
    let conn = client.connect(server_addr, b"my-proto").await?;
    let (mut send, mut recv) = conn.open_bi().await?;
    send.write_all(b"hello").await?;
    send.finish().await?;
    
    let mut resp = Vec::new();
    recv.read_to_end(&mut resp).await?;
    println!("Client got: {:?}", std::str::from_utf8(&resp));
    Ok(())
}

Custom Relay and Transport Configuration

For self-hosted infrastructure or specific network requirements:

use iroh::{
    endpoint::{Builder, RelayMode, presets},
    RelayUrl,
};
use std::collections::HashMap;

let custom_relay: RelayUrl = "https://my-relay.example.com".parse()?;

let endpoint = Builder::new(presets::Minimal)
    .relay_mode(RelayMode::Custom(
        HashMap::from([(custom_relay.clone(), Default::default())]),
    ))
    .clear_ip_transports()
    .bind_addr_with_opts(
        "192.0.2.10:0",
        iroh::endpoint::BindOpts::default()
            .set_prefix_len(24)
            .set_is_default_route(true),
    )?
    .portmapper_config(PortmapperConfig::Disabled)
    .bind()
    .await?;

Summary

  • Use presets (presets::N0 or presets::Minimal) from iroh/src/endpoint/presets.rs to establish sane defaults and avoid crypto provider misconfiguration.
  • Explicitly configure relay modes via Builder::relay_mode in iroh/src/endpoint.rs (lines 542-574) to control relay server connectivity.
  • Manage external addresses through external_addr() and add_external_addr() to improve NAT traversal.
  • Disable the portmapper in containerized environments using PortmapperConfig::Disabled from iroh/src/portmapper.rs.
  • Configure TLS via iroh/src/tls.rs when not using presets, ensuring you provide a valid crypto provider.
  • Set specific ALPN identifiers to prevent protocol conflicts on QUIC connections.
  • Implement hooks from iroh/src/endpoint/hooks.rs for connection auditing and policy enforcement.
  • Always call close().await before dropping the endpoint to ensure clean resource cleanup.

Frequently Asked Questions

What is the difference between presets::N0 and presets::Minimal?

presets::N0 bundles the default Number-0 relay configuration, DNS address lookup via PKARR, and the ring crypto provider, making it ideal for general internet connectivity. presets::Minimal provides a stripped-down foundation without pre-configured relays or address lookup services, requiring you to manually specify each component in iroh/src/endpoint/presets.rs. Use Minimal when you need complete control over network dependencies or are building specialized LAN-only applications.

When should I disable the portmapper in Iroh?

Disable the portmapper using builder.portmapper_config(PortmapperConfig::Disabled) when running in containerized environments, restrictive corporate networks, or any scenario where UPnP/NAT-PMP is unavailable or prohibited. According to iroh/src/portmapper.rs, enabling portmapping without user consent can unintentionally expose services through gateway routers, so it should only be activated when explicitly needed for NAT traversal in trusted environments.

How do I configure custom relays in Iroh?

Use RelayMode::Custom with a HashMap<RelayUrl, RelaySettings> to point at corporate or self-hosted relay infrastructure. As implemented in iroh/src/endpoint.rs lines 542-574, the Builder::relay_mode method converts this configuration into transport-layer relay connections. Combine this with clear_ip_transports() if you want to replace rather than supplement the default relay map.

What is the purpose of ALPN in Iroh endpoints?

ALPN (Application-Layer Protocol Negotiation) vectors identify your specific protocol implementation on the QUIC connection, preventing conflicts with other services that might use the same port. Set ALPNs via builder.alpns() or endpoint.set_alpns() using specific byte strings like b"my-app/1.0" rather than generic identifiers. This ensures that incoming connections are routed to the correct protocol handler within your application.

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 →