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

> Learn to enable custom transport protocols in Iroh by activating the unstable-custom-transports feature. Implement essential traits and register your transport for enhanced networking capabilities.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-18

---

**Enable the `unstable-custom-transports` feature in your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) dependency declaration:

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

```

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

```bash
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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs), it requires a single method:

```rust
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()`:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/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()`:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs):

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils.rs) and the complete working example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) to unlock the custom transport API in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs) and the associated APIs in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.