How to Use Custom Transports in iroh with the unstable-custom-transports Feature

Enable the unstable-custom-transports Cargo feature to plug user-defined networking layers into iroh's connection stack, allowing you to implement the CustomTransport trait and prioritize custom paths via a PathSelector when building an Endpoint.

The iroh networking library provides a pluggable transport architecture that extends its default IPv4/IPv6 and relay capabilities with alternative packet movers. By activating the unstable-custom-transports feature in the n0-computer/iroh repository, you gain access to the low-level APIs necessary to inject Bluetooth, Tor overlays, or in-memory test networks directly into the connection lifecycle.

Enabling the Feature Flag

To access the custom transport APIs, add the feature to your Cargo.toml:

[dependencies]
iroh = { version = "0.x", features = ["unstable-custom-transports"] }

When enabled, the transports submodule becomes available through iroh::endpoint, exposing the traits required to define new transport layers.

Core Architecture and Types

The custom transport system centers on types re-exported in iroh/src/endpoint.rs. According to the source code (lines 41-46), the module publicly exposes:

  • CustomTransport: The trait your implementation must satisfy
  • CustomEndpoint: Represents the local endpoint of your custom transport
  • CustomSender: Handles raw byte transmission for your protocol

Each transport is identified by a unique u16 transport ID. The repository maintains a canonical registry in [TRANSPORTS.md](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md) where the internal test transport occupies ID 0x20 (lines 5-9). Before deploying a production transport, allocate a new ID by submitting a PR to this registry.

Implementing the CustomTransport Trait

A functional implementation requires defining address formatting and connection logic. The reference TestTransport in iroh/src/test_utils/test_transport.rs demonstrates the interface:

  • addr(): Returns a CustomAddr containing your transport ID and opaque address data
  • connect(): Establishes a stream and returns a CustomSender capable of sending and receiving raw packets

Your implementation handles the wire protocol—whether Bluetooth L2CAP frames, QUIC overlays, or encapsulated UDP—while iroh manages higher-level connection state.

Configuring the Endpoint Builder

To activate your transport, use the Endpoint::builder() pattern. The implementation in iroh/src/endpoint.rs exposes methods to inject transports and control path selection:

  1. Inject the transport using .preset() as shown in the custom-transport example (lines 71-87)
  2. Optionally disable defaults with .clear_ip_transports() and .clear_relay_transports() to force traffic through your layer
  3. Set a path selector via .path_selector() to prioritize your transport over built-in options

The example configuration follows this pattern:

let mut builder = Endpoint::builder(presets::N0)
    .secret_key(secret_key)
    .preset(transport)               // Inject the custom transport
    .path_selector(Arc::new(PreferTestTransport));
if !self.keep_ip { 
    builder = builder.clear_ip_transports(); 
}
if !self.keep_relay { 
    builder = builder.clear_relay_transports(); 
}

Writing a PathSelector

The PathSelector trait determines which network path iroh uses for a connection. To prefer your custom transport, implement the trait to check for Addr::Custom variants. The PreferTestTransport implementation in the example (lines 37-69) shows the selection logic:

impl PathSelector for PreferTestTransport {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        let mut selection = PathSelection::none();
        
        // Prioritize paths using the custom transport ID
        if let Some(p) = ctx.paths().find(|p| {
            matches!(
                p.network_path().remote(), 
                Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID
            )
        }) {
            selection.set(&p);
            return selection;
        }
        
        // Fall back to lowest-RTT path otherwise
        // ...
        selection
    }
}

Complete Usage Example

Below is a practical example binding an endpoint with a test transport and forcing it to win path selection:

use iroh::{Endpoint, SecretKey};
use iroh::endpoint::{Builder, PathSelector, PathSelectionContext, PathSelection, Addr};
use std::sync::Arc;

// From iroh/src/test_utils/test_transport.rs
use iroh::test_utils::test_transport::{TestTransport, TEST_TRANSPORT_ID};

struct PreferCustomTransport;

impl PathSelector for PreferCustomTransport {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        let mut sel = PathSelection::none();
        if let Some(p) = ctx.paths().find(|p| {
            matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID)
        }) {
            sel.set(&p);
        }
        sel
    }
}

fn build_endpoint(secret: SecretKey, transport: Arc<TestTransport>) -> Builder {
    Endpoint::builder(presets::N0)
        .secret_key(secret)
        .preset(transport)                    // Inject custom transport
        .path_selector(Arc::new(PreferCustomTransport))
        .clear_ip_transports()                // Optional: disable IPv4/IPv6
        .clear_relay_transports()             // Optional: disable relay
}

When executed, the connection resolves through the custom transport layer, verified by checking the selected path type (lines 66-74 in the example).

Summary

  • Enable unstable-custom-transports in Cargo.toml to unlock the custom transport API
  • Implement CustomTransport in iroh/src/test_utils/test_transport.rs to define packet movement and addressing
  • Register your transport with a unique 16-bit ID documented in TRANSPORTS.md
  • Use Endpoint::builder() with .preset() to inject your implementation
  • Implement PathSelector to prioritize Addr::Custom paths over standard IP or relay connections
  • Reference the custom-transport example for working integration patterns

Frequently Asked Questions

What version of iroh supports unstable-custom-transports?

The feature is available in recent mainline builds and 0.x releases, though marked unstable because the API may evolve. Check the repository's Cargo.toml for the exact feature flag definition and ensure you're tracking a compatible git revision.

Can I use multiple custom transports simultaneously?

Yes. The Builder accumulates transports via multiple .preset() calls, and the PathSelector receives all available paths including multiple custom types. Your selector logic can prioritize between them based on transport ID or other path quality metrics.

How do I allocate a new transport ID for my protocol?

Submit a pull request adding your transport ID and description to TRANSPORTS.md. IDs are u16 values where the range is split between reserved internal transports, experimental user transports, and stable allocations. Using an unregistered ID risks collisions with future iroh updates.

Does enabling custom transports affect existing IPv4/IPv6 connections?

No, built-in transports remain active unless explicitly cleared. Call .clear_ip_transports() and .clear_relay_transports() on the builder to restrict the endpoint to only your custom implementation, or leave them enabled to allow fallback when your transport is unavailable.

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 →