# How to Configure Custom Transports in iroh with unstable-custom-transports

> Configure custom transports in iroh using the unstable-custom-transports feature. Learn to inject implementations and drive path selection for tailored network solutions.

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

---

**Enable the `unstable-custom-transports` Cargo feature to unlock `CustomTransport`, `CustomEndpoint`, and `CustomSender`, then inject your implementation into the `Endpoint` builder and drive path selection with a custom `PathSelector`.**

The `n0-computer/iroh` networking stack supports pluggable transports through an unstable feature flag that exposes low-level transport abstractions. By enabling `unstable-custom-transports`, you can configure custom transports in iroh to route packets over user-defined networks alongside or in place of the built-in IPv4, IPv6, and relay paths.

## How Custom Transports Work in iroh

Iroh identifies every networking strategy with a **`u16` transport-id**. The canonical registry lives in [[`TRANSPORTS.md`](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md)](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md), which reserves identifiers such as `0x20` for the internal test transport. When the `unstable-custom-transports` feature is active, [[`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) re-exports the custom transport API from the socket layer.

Lines 29-48 define the `transports` sub-module, and lines 41-46 pull in the structs you must implement:

- `CustomTransport` — the trait your networking logic implements.
- `CustomEndpoint` — represents the local side of a custom transport.
- `CustomSender` — the handle for sending and receiving raw bytes.

## Implementing a Custom Transport

A custom transport must provide local addressing and outbound connection capabilities. The reference implementation, `TestTransport`, lives in [`iroh/src/test_utils/test_transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs) and demonstrates the required interface.

The transport returns a **`CustomAddr`** via its `addr()` method. This address contains the transport-id and opaque address data. When the `Endpoint` needs to send data, it calls `connect()`, which yields a **`CustomSender`** that moves raw packets across your underlying network. The low-level definitions for these types reside in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs), which exposes the `TransportConfig` enum and the `Addr` and `CustomAddr` wrappers.

## Configuring the Endpoint Builder

Pass your transport to **`Endpoint::builder()`** to include it in the local endpoint. The example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) demonstrates the full pattern. You inject the transport with `.preset()` and strip default paths with `.clear_ip_transports()` and `.clear_relay_transports()`.

```rust
let mut builder = Endpoint::builder(presets::N0)
    .secret_key(secret_key)
    .preset(transport)               // inject the custom transport
    .path_selector(Arc::new(PreferTestTransport));

if !keep_ip {
    builder = builder.clear_ip_transports();
}
if !keep_relay {
    builder = builder.clear_relay_transports();
}

let endpoint = builder.bind().await?;

```

After binding, the `Endpoint` holds your custom transport in its internal `Vec<TransportConfig>` alongside any remaining default transports.

## Driving Path Selection with PathSelector

By default, iroh ranks paths by latency. To force traffic over your custom transport, implement **`PathSelector`** and attach it to the builder. The example defines `PreferTestTransport` to prefer any path whose remote address is `Addr::Custom` with the test transport id, falling back to the lowest-RTT path otherwise.

```rust
impl PathSelector for PreferTestTransport {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        let mut selection = PathSelection::none();

        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;
        }

        // fallback to lowest-RTT path
        if let Some(p) = ctx.paths().min_by_key(|p| p.rtt().unwrap_or(Duration::MAX)) {
            selection.set(&p);
        }

        selection
    }
}

```

The `PathSelectionContext` exposes candidate paths via `paths()`. Each path exposes its remote address through `network_path().remote()`. Matching on `Addr::Custom` lets you prioritize your transport while preserving fallback behavior.

## Minimal Runnable Setup

The following excerpt combines the builder configuration and selector into a single setup function. It assumes you have a `SecretKey` and an `Arc<dyn CustomTransport>` already initialized.

```rust
use iroh::{
    Endpoint, SecretKey,
    endpoint::{Builder, PathSelector, PathSelection, PathSelectionContext},
    transports::{Addr, CustomTransport},
};
use std::sync::Arc;
use std::time::Duration;

const TEST_TRANSPORT_ID: u16 = 0x20;

struct PreferTestTransport;

impl PathSelector for PreferTestTransport {
    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);
            return sel;
        }
        if let Some(p) = ctx.paths().min_by_key(|p| p.rtt().unwrap_or(Duration::MAX)) {
            sel.set(&p);
        }
        sel
    }
}

fn builder_with_custom(secret: SecretKey, transport: Arc<dyn CustomTransport>) -> Builder {
    Endpoint::builder(presets::N0)
        .secret_key(secret)
        .preset(transport)
        .path_selector(Arc::new(PreferTestTransport))
        .clear_ip_transports()
        .clear_relay_transports()
}

```

After calling `.bind().await`, invoke `endpoint.connect()` to establish a connection. The discovery layer resolves the custom address, and your selector ensures the custom path wins.

## Summary

- **Enable `unstable-custom-transports`** in [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) to expose `CustomTransport`, `CustomEndpoint`, and `CustomSender` from [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).
- **Reserve a transport-id** in [`TRANSPORTS.md`](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md) so the stack can identify your custom addresses.
- **Implement `CustomTransport`** to provide local addresses and outbound connection handles via `CustomSender`.
- **Attach the transport** to an `Endpoint` builder with `.preset()` and optionally remove built-in transports with `.clear_ip_transports()` and `.clear_relay_transports()`.
- **Control routing** by implementing `PathSelector` and matching on `Addr::Custom` inside `select()` to prioritize your transport.

## Frequently Asked Questions

### What Cargo feature enables custom transports in iroh?

The `unstable-custom-transports` feature unlocks the custom transport API. When enabled, [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) re-exports `CustomTransport`, `CustomEndpoint`, and `CustomSender` from the underlying socket layer, allowing you to inject user-defined networking logic into the `Endpoint` builder.

### How does iroh identify a custom transport?

Each custom transport is identified by a unique **`u16` transport-id** defined in [`TRANSPORTS.md`](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md). Addresses carry this id inside `CustomAddr`, and the `PathSelector` inspects `Addr::Custom` to distinguish custom paths from standard IP or relay paths.

### Can custom transports replace IP and relay transports entirely?

Yes. The `Endpoint` builder provides **`clear_ip_transports()`** and **`clear_relay_transports()`** to remove the default networking paths. If you clear these and supply only a custom transport via `.preset()`, iroh will route all traffic exclusively through your implementation.

### Where can I find a working example of a custom transport?

The official example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) demonstrates an end-to-end setup using `TestTransport` from [`iroh/src/test_utils/test_transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs). It configures the builder, attaches a `PreferTestTransport` selector, and verifies that the active path uses the custom transport id.