How to Enable Custom Transport Protocols in Iroh with the `unstable-custom-transports` Feature

Enable the unstable-custom-transports feature in your Cargo.toml, implement the CustomTransport, CustomEndpoint, and CustomSender traits from iroh::socket::transports::custom, then register your transport via Builder::add_custom_transport() before binding the endpoint.

Iroh is a Rust-based peer-to-peer networking stack that defaults to QUIC but allows you to plug in custom transport protocols for sending and receiving raw packets. This capability is gated behind the unstable-custom-transports Cargo feature, which exposes the trait interfaces necessary to integrate proprietary radio links, VPN tunnels, or simulation layers directly into Iroh's connection model according to the n0-computer/iroh source code.

Enable the unstable-custom-transports Feature

To compile Iroh with custom transport support, add the feature to your Cargo.toml dependency declaration:

[dependencies]
iroh = { version = "0.20", features = ["unstable-custom-transports"] }

Alternatively, build from the command line when working with the repository directly:

cargo build --features unstable-custom-transports

What the Feature Activates

Enabling this feature unlocks three critical components in the Iroh source:

  1. Trait definitions in iroh/src/socket/transports/custom.rs — Defines the CustomTransport, CustomEndpoint, and CustomSender traits that your implementation must satisfy.
  2. Builder API extensions in iroh/src/endpoint.rs (lines 800–814) — Adds the Builder::add_custom_transport() method and the internal TransportConfig::Custom variant.
  3. Public re-exports in iroh/src/endpoint.rs (module transports at line 29) — Makes the custom transport types available under iroh::endpoint::transports for downstream consumption.

Implement the Three Core Traits

Custom transports require implementing three hierarchical traits that handle binding, address advertisement, and packet transmission.

CustomTransport

The CustomTransport trait acts as a factory for creating endpoints. As defined in iroh/src/socket/transports/custom.rs, it requires a single method:

fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>>;

This method is called during Builder::bind() to initialize your transport.

CustomEndpoint

The CustomEndpoint trait manages the local side of the connection. Key methods include:

  • watch_local_addrs() — Returns a watcher yielding Vec<CustomAddr> that are advertised to remote peers through Iroh's discovery mechanisms.
  • create_sender() — Returns an Arc<dyn CustomSender> used to transmit packets.
  • poll_recv() — Polls for inbound packets, filling the provided buffers and RecvInfo metadata for Iroh's path selection machinery.

CustomSender

The CustomSender trait handles actual packet transmission to remote addresses:

  • is_valid_send_addr() — Validates whether a given CustomAddr is reachable by this sender.
  • poll_send() — Transmits a packet to the specified destination, returning Poll<io::Result<()>>.

Register Your Transport with the Endpoint Builder

After implementing the traits, instantiate your transport and register it using Builder::add_custom_transport() before calling bind():

use std::sync::Arc;
use iroh::Endpoint;

let my_transport = Arc::new(MyCustomTransport::new());
let endpoint = Endpoint::builder()
    .add_custom_transport(my_transport)
    .bind()
    .await?;

The add_custom_transport method, implemented in iroh/src/endpoint.rs, stores your transport as an Arc<dyn CustomTransport> inside the builder's internal transports vector.

Control Path Selection with a Custom PathSelector

By default, Iroh selects paths based on measured latency. To prioritize your custom transport over standard IP or relay paths, implement the PathSelector trait and attach it via Builder::path_selector():

use iroh::endpoint::PathSelector;

struct PreferCustomTransport;

impl PathSelector for PreferTestTransport {
    // Implementation that prefers TransportAddr::Custom
}

builder
    .add_custom_transport(my_transport)
    .path_selector(Arc::new(PreferCustomTransport));

For a complete reference implementation, see the PreferTestTransport example in iroh/examples/custom-transport.rs.

Complete Minimal Example

Below is a minimal echo transport demonstrating the structure required by the traits in iroh/src/socket/transports/custom.rs:

use std::io;
use std::sync::Arc;
use std::task::{Context, Poll};
use iroh::socket::transports::custom::{CustomTransport, CustomEndpoint, CustomSender};
use iroh_base::CustomAddr;

struct EchoTransport;

impl CustomTransport for EchoTransport {
    fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
        Ok(Box::new(EchoEndpoint))
    }
}

struct EchoEndpoint;

impl CustomEndpoint for EchoEndpoint {
    fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
        n0_watcher::Direct::new(vec![CustomAddr::new(0, vec![0])])
    }

    fn create_sender(&self) -> Arc<dyn CustomSender> {
        Arc::new(EchoSender)
    }

    fn poll_recv(
        &mut self,
        _cx: &mut Context,
        _bufs: &mut [io::IoSliceMut<'_>],
        _metas: &mut [noq_udp::RecvMeta],
        _infos: &mut [iroh::socket::transports::RecvInfo],
    ) -> Poll<io::Result<usize>> {
        Poll::Ready(Ok(0))
    }
}

struct EchoSender;

impl CustomSender for EchoSender {
    fn is_valid_send_addr(&self, _addr: &CustomAddr) -> bool {
        true
    }

    fn poll_send(
        &self,
        _cx: &mut Context,
        _dst: &CustomAddr,
        _src: Option<&CustomAddr>,
        _transmit: &iroh::socket::transports::Transmit<'_>,
    ) -> Poll<io::Result<()>> {
        Poll::Ready(Ok(()))
    }
}

Reference Implementation and Architecture

For a production-ready example, examine the test transport in iroh/src/test_utils.rs and the complete working example in iroh/examples/custom-transport.rs. The latter demonstrates a TestNetwork that simulates packet flow between endpoints.

When you call Builder::bind(), Iroh executes the following sequence from iroh/src/endpoint.rs:

  1. Iterates over the transports vector containing your TransportConfig::Custom.
  2. Invokes CustomTransport::bind() to create a Box<dyn CustomEndpoint>.
  3. Polls CustomEndpoint::poll_recv() in the runtime to receive packets via RecvInfo.
  4. Selects paths using your PathSelector, preferring TransportAddr::Custom when specified.
  5. Transmits via CustomSender::poll_send() when the custom path is chosen.

Custom addresses returned by watch_local_addrs() are advertised through standard discovery services (pkarr, DNS) as TransportAddr::Custom, allowing compatible peers to select them automatically.

Summary

  • Enable the unstable-custom-transports feature in Cargo.toml to unlock the custom transport API in iroh/src/socket/transports/custom.rs.
  • Implement three traits: CustomTransport (factory), CustomEndpoint (receiving), and CustomSender (sending).
  • Register your transport via Builder::add_custom_transport() defined in iroh/src/endpoint.rs before calling bind().
  • Optionally provide a PathSelector via Builder::path_selector() to prioritize custom transports over IP or relay paths.
  • Custom addresses are automatically advertised and discovered through Iroh's standard address resolution mechanisms.

Frequently Asked Questions

Is the unstable-custom-transports feature safe for production use?

No. As indicated by the "unstable" prefix, this feature is explicitly excluded from semantic versioning guarantees. The trait definitions in iroh/src/socket/transports/custom.rs and the associated APIs in iroh/src/endpoint.rs may change in breaking ways without a major version bump, potentially requiring updates to your implementation when upgrading Iroh.

Can I use multiple custom transports simultaneously?

Yes. The Builder supports adding multiple transports via repeated calls to add_custom_transport(). Each transport is stored in the internal transports vector and polled independently during the binding process in iroh/src/endpoint.rs. This allows you to implement fallback strategies or support multiple underlying link types in a single endpoint.

How does Iroh prioritize between custom transports and standard QUIC paths?

Iroh uses the PathSelector trait to determine the optimal path for each connection. Without a custom selector, the default implementation typically prefers the lowest-latency available path. You can override this by providing your own selector via Builder::path_selector() that prioritizes TransportAddr::Custom variants when they are available for a given remote endpoint.

Do peers need to enable the same feature to communicate over a custom transport?

Yes. Both endpoints must enable unstable-custom-transports and implement compatible transport logic. The custom addresses are advertised as TransportAddr::Custom through the discovery system, and only peers with the feature enabled can recognize and select these paths during connection establishment. If a peer lacks the feature, it will ignore the custom address and use standard IP or relay paths instead.

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 →