How to Implement Custom Transport Layers in iroh Using the Unstable Custom Transports Feature

Enable the unstable-custom-transports Cargo feature to extend iroh's networking stack with custom transports by implementing the CustomTransport trait, registering a unique transport ID, and using a custom PathSelector to prioritize your transport during path selection.

Iroh provides a pluggable networking architecture that allows developers to integrate alternative packet transport mechanisms alongside built-in IPv4/IPv6 and relay protocols. By enabling the unstable-custom-transports feature, you can implement custom transport layers that work within iroh's connection establishment and path selection logic, supporting use cases from Bluetooth overlays to Tor tunnels.

Enabling the Unstable Feature

Custom transports are gated behind the unstable-custom-transports Cargo feature flag. Add the following to your Cargo.toml to access the transport extension APIs:

[dependencies]
iroh = { git = "https://github.com/n0-computer/iroh", features = ["unstable-custom-transports"] }

When enabled, the Endpoint module re-exports the necessary types from the internal socket layer. In [iroh/src/endpoint.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs#L29-L48), the transports sub-module exposes CustomTransport, CustomEndpoint, and CustomSender (lines 41-46).

Understanding the Transport Architecture

A custom transport in iroh is identified by a transport ID (a u16 value). The project maintains a canonical registry in [TRANSPORTS.md](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md#L5-L9), where 0x20 is reserved for the internal test transport.

The architecture revolves around three core abstractions defined in the socket layer:

  • CustomTransport – The trait you implement to define packet sending/receiving logic
  • CustomAddr – A structured address containing the transport ID and opaque endpoint-specific data
  • PathSelector – The mechanism that chooses which transport path to use for a given connection

Implementing the CustomTransport Trait

Your transport implementation must satisfy the CustomTransport interface expected by the socket layer. While the internal trait definition resides in the socket transports module, you can reference the test implementation in [iroh/src/test_utils/test_transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs) as a working template.

A minimal implementation requires:

  1. Address generation – Construct a CustomAddr containing your transport ID and endpoint-specific metadata
  2. Connection establishment – Return a CustomSender capable of sending and receiving raw bytes
  3. Transport identification – Return your allocated transport ID when queried

Configuring the Endpoint Builder

To activate your custom transport, inject it during Endpoint construction using the builder API. The example in [iroh/examples/custom-transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) demonstrates this pattern in the configure method (lines 71-87):

use iroh::{Endpoint, endpoint::Builder, SecretKey};
use std::sync::Arc;

fn builder_with_custom(
    secret_key: SecretKey,
    transport: Arc<dyn CustomTransport>,
) -> Builder {
    Endpoint::builder(presets::N0)
        .secret_key(secret_key)
        .preset(transport)  // Inject the custom transport
        .clear_ip_transports()      // Optional: disable IPv4/IPv6
        .clear_relay_transports()   // Optional: disable relay fallback
}

The .preset() method adds your transport to the internal Vec<TransportConfig> maintained by the builder, while clear_ip_transports() and clear_relay_transports() allow you to force exclusive use of your custom implementation.

Creating a Custom Path Selector

By default, iroh uses RTT-based path selection. To prioritize your custom transport, implement the PathSelector trait and inspect the PathSelectionContext for custom addresses. The reference implementation in [iroh/examples/custom-transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs#L37-L69) shows a selector that prefers the test transport:

use iroh::endpoint::{PathSelector, PathSelection, PathSelectionContext, transports::Addr};

const TEST_TRANSPORT_ID: u16 = 0x20;

struct PreferTestTransport;

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

Attach the selector to your builder using .path_selector(Arc::new(PreferTestTransport)).

Complete Working Example

Putting the components together, here is a minimal example that establishes a connection using only the custom test transport:

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

// Assume TestTransport implements CustomTransport
use iroh::test_utils::test_transport::{TestTransport, TEST_TRANSPORT_ID};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let secret = SecretKey::generate();
    let transport = Arc::new(TestTransport::new());
    
    // Build endpoint with custom transport
    let endpoint = Endpoint::builder(presets::N0)
        .secret_key(secret)
        .preset(transport)
        .path_selector(Arc::new(PreferTestTransport))
        .clear_ip_transports()
        .clear_relay_transports()
        .bind()
        .await?;
    
    // Connect using the custom transport path
    let conn = endpoint.connect(node_id, ALPN).await?;
    Ok(())
}

This configuration forces the endpoint to use only your custom transport implementation, bypassing standard networking paths entirely.

Key Source Files

Understanding the following source locations is essential for implementing custom transports:

Summary

  • Enable the feature flag unstable-custom-transports in your Cargo.toml to access extension APIs
  • Implement CustomTransport to define packet transport logic and address formats
  • Register a transport ID in TRANSPORTS.md to avoid collisions with other implementations
  • Use the builder API to inject your transport via .preset() and optionally disable built-in transports with .clear_ip_transports() and .clear_relay_transports()
  • Implement PathSelector to prioritize your custom transport over IPv4/IPv6 paths during connection establishment
  • Reference the test implementation in test_transport.rs and the working example in custom-transport.rs for concrete trait implementations

Frequently Asked Questions

What does the unstable-custom-transports feature enable?

The feature unlocks the CustomTransport trait and associated types in the public API, allowing you to inject arbitrary packet transport mechanisms into iroh's networking stack. Without this flag, the transport extension points are hidden and the necessary builder methods are unavailable.

How do I reserve a transport ID for my implementation?

Transport IDs are 16-bit unsigned integers (u16) tracked in the [TRANSPORTS.md](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md) file at the repository root. To avoid collisions, submit a pull request that registers your ID with a description of your transport mechanism. The range 0x0001-0x00FF is reserved for experimental use, while 0x20 is allocated for the internal test transport.

Can I use multiple custom transports simultaneously?

Yes. The Endpoint builder accepts multiple transports via repeated calls to .preset(). Each transport must have a unique ID. The PathSelector implementation determines which transport wins for a given connection by inspecting the Addr::Custom variant in the path selection context and choosing based on your priority logic.

What is the difference between CustomEndpoint and CustomSender?

CustomEndpoint represents the local binding of your transport (analogous to a socket), while CustomSender is the per-connection handle returned by your transport's connect method that actually transmits and receives byte streams. The endpoint manages the local address and incoming connection state, whereas the sender handles the active data flow for established connections.

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 →