# How to Integrate iroh with Custom Transport Layers (Unstable Feature)

> Integrate iroh with custom transport layers by implementing traits, registering, and configuring PathSelector. Unstable feature, requires enabling the unstable-custom-transports flag.

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

---

**Integrating iroh with custom transport layers requires implementing the `CustomTransport` and `CustomEndpoint` traits, registering the transport via `add_custom_transport` on the `Endpoint` builder, and configuring a `PathSelector` to prioritize your transport when available, all gated behind the `unstable-custom-transports` feature flag.**

The iroh networking library provides a pluggable transport architecture that allows developers to integrate custom networking layers beyond standard UDP and relay connections. This capability enables Bluetooth, LoRa, in-memory testing channels, or proprietary protocols to plug into iroh's QUIC-based API. By implementing the core traits defined in [`iroh/src/endpoint/transports/mod.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/transports/mod.rs) and registering your transport with the endpoint builder, you can extend iroh's path selection logic to include your custom networking medium.

## Enable the Unstable Feature

Custom transports are deliberately gated behind the `unstable-custom-transports` feature flag to allow API iteration without breaking stable releases. To use this functionality, enable the feature in your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml):

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

```

For development and testing, you can also run examples directly:

```bash
cargo run --example custom-transport --features unstable-custom-transports

```

## Architecture Overview

The custom transport integration follows a four-stage architecture:

1. **Transport Registration** – Add your transport to the `Endpoint` builder using `add_custom_transport`, which stores the transport in the builder's internal `Preset` configuration.
2. **Address Discovery** – Custom transports expose `CustomAddr` values (`TransportAddr::Custom`) that are included in the peer's `EndpointAddr` during discovery.
3. **Path Selection** – When establishing connections, iroh evaluates candidate paths via the `PathSelector` trait. You can implement a custom selector (like `PreferTestTransport`) to prioritize your transport over standard IP paths.
4. **Sending and Receiving** – The transport implements `CustomSender` for packetization and transmission, while `CustomEndpoint` handles local address monitoring and packet reception via `poll_recv()`.

## Implementing the Transport Traits

A complete custom transport requires implementing three core traits defined in [`iroh/src/endpoint/transports/mod.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/transports/mod.rs).

### CustomTransport and CustomEndpoint

Your transport struct must implement both `CustomTransport` (for binding) and `CustomEndpoint` (for runtime operations). The reference implementation in [`iroh/src/test_utils/test_transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs) demonstrates the required methods:

```rust
pub const TEST_TRANSPORT_ID: u64 = 0x20;

pub struct TestTransport {
    id_watchable: n0_watcher::Watchable<Vec<CustomAddr>>,
    network: TestNetwork,
    id: EndpointId,
}

impl CustomTransport for TestTransport {
    fn bind(&self, _socket: Option<std::net::UdpSocket>) -> Result<Arc<dyn CustomEndpoint>> {
        // Return self as the endpoint
        Ok(Arc::new(self.clone()))
    }
}

impl CustomEndpoint for TestTransport {
    fn watch_local_addrs(&self) -> Watchable<Vec<CustomAddr>> {
        self.id_watchable.clone()
    }
    
    fn poll_recv(&self, cx: &mut Context) -> Poll<Result<RecvMeta>> {
        // Implement packet reception logic
        // ...
    }
}

```

### CustomSender

The `CustomSender` trait handles packet transmission. According to the test transport implementation, you must implement `poll_send()` to handle actual packet dispatch:

```rust
impl CustomSender for TestSender {
    fn poll_send(&self, cx: &mut Context, transmits: &[Transmit]) -> Poll<Result<usize>> {
        // Packetize and send via your underlying channel
        // Return number of packets sent
        // ...
    }
}

```

### Address Handling

Custom transports use `CustomAddr::from_parts()` to construct addresses with a unique transport ID and endpoint identifier:

```rust
fn to_custom_addr(&self) -> CustomAddr {
    CustomAddr::from_parts(TEST_TRANSPORT_ID, self.id.as_bytes())
}

```

This allows the transport to identify its own addresses during path selection.

## Registering with the Endpoint Builder

Register your transport using the `Endpoint` builder API in [`iroh/src/endpoint/builder.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/builder.rs). The example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) shows the complete registration pattern:

```rust
let network = TestNetwork::new();
let secret = SecretKey::generate();

let transport = network.create_transport(secret.public())?;

let builder = Endpoint::builder(presets::N0)
    .secret_key(secret)
    .add_custom_transport(transport.clone())
    .path_selector(Arc::new(PreferTestTransport));

```

The `Preset` implementation (visible in [`test_transport.rs`](https://github.com/n0-computer/iroh/blob/main/test_transport.rs)) automatically forwards the transport to `add_custom_transport()` when using the `.preset()` method:

```rust
impl Preset for Arc<TestTransport> {
    fn apply(self, builder: Builder) -> Builder {
        builder
            .add_custom_transport(self.clone())
            .address_lookup(self.network.address_lookup())
    }
}

```

## Path Selection Strategies

The `PathSelector` trait in [`iroh/src/endpoint/path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/path_selector.rs) determines which transport wins when multiple paths exist. The [`custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/custom-transport.rs) example provides a `PreferTestTransport` implementation that prioritizes custom transport addresses:

```rust
struct PreferTestTransport;

impl PathSelector for PreferTestTransport {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        // First, try to find any candidate on our custom transport
        if let Some(p) = ctx.paths().find(|p| {
            matches!(p.network_path().remote(),
                     Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID)
        }) {
            let mut sel = PathSelection::none();
            sel.set(&p);
            return sel;
        }

        // Fallback: pick the lowest-RTT path
        ctx.paths()
            .filter_map(|p| p.stats().map(|s| (p, s.rtt)))
            .min_by_key(|(_, rtt)| *rtt)
            .map(|(p, _)| {
                let mut sel = PathSelection::none();
                sel.set(p);
                sel
            })
            .unwrap_or_default()
    }
}

```

Alternatively, you can use the built-in `BiasedRttPathSelector` with a `TransportBias` configuration to prefer your custom transport's `AddrKind` without implementing a custom selector.

## Complete Integration Example

The following end-to-end example from [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) demonstrates two endpoints communicating over an in-memory custom transport:

```rust
#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    // Build two endpoints that talk over the test transport
    let network = TestNetwork::new();
    let s1 = SecretKey::from([0u8; 32]);
    let s2 = SecretKey::from([1u8; 32]);

    let t1 = network.create_transport(s1.public())?;
    let t2 = network.create_transport(s2.public())?;

    let ep1 = Endpoint::builder(presets::N0)
        .secret_key(s1)
        .preset(t1)
        .path_selector(Arc::new(PreferTestTransport))
        .bind().await?;

    let ep2 = Endpoint::builder(presets::N0)
        .secret_key(s2)
        .preset(t2)
        .bind().await?;

    // Set up a simple echo protocol
    let server = Router::builder(ep2)
        .accept(ALPN, Echo)
        .spawn();

    // Connect using only the endpoint ID (discovery returns the custom address)
    let conn = ep1.connect(s2.public(), ALPN).await?;

    // Verify that the selected path is the custom transport
    assert!(conn.paths().iter().any(|p|
        p.is_selected() && matches!(p.remote_addr(),
        TransportAddr::Custom(a) if a.id() == TEST_TRANSPORT_ID)));

    // Exchange a message
    let (mut send, mut recv) = conn.open_bi().await?;
    send.write_all(b"hello").await?;
    send.finish()?;
    let reply = recv.read_to_end(100).await?;
    assert_eq!(reply, b"hello");

    conn.close(0u32.into(), b"bye");
    server.shutdown().await?;
    Ok(())
}

```

## Summary

- **Enable the feature** using `unstable-custom-transports` in your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) or cargo commands.
- **Implement three traits**: `CustomTransport` for binding, `CustomEndpoint` for address watching and reception, and `CustomSender` for transmission.
- **Register via the builder** using `add_custom_transport()` or the `Preset` pattern demonstrated in [`iroh/src/test_utils/test_transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs).
- **Control path selection** by implementing `PathSelector` or using `BiasedRttPathSelector` with transport bias to prioritize your custom transport.
- **Verify integration** by checking that `TransportAddr::Custom` appears in the connection's selected paths.

## Frequently Asked Questions

### What is the stability status of custom transports in iroh?

Custom transports are marked as unstable and require the `unstable-custom-transports` feature flag. This allows the iroh team to iterate on the `CustomTransport`, `CustomEndpoint`, and `CustomSender` trait APIs without breaking semantic versioning guarantees for the stable crate surface.

### How does path selection work with custom transports?

When a connection is established, iroh evaluates all available paths (IP, relay, and custom) through the `PathSelector` trait. The selector receives a `PathSelectionContext` containing all candidate paths and their statistics. You can implement a custom selector to prefer `TransportAddr::Custom` addresses, or use the built-in biased RTT selector with a `TransportBias` configuration.

### Can I use custom transports alongside standard IP and relay transports?

Yes. The builder API supports hybrid configurations. You can call `add_custom_transport()` while retaining the default IP and relay transports from `presets::N0`, or explicitly clear them using `clear_ip_transports()` and `clear_relay_transports()` if you want an isolated custom transport network.

### What methods must a custom transport implement?

According to [`iroh/src/endpoint/transports/mod.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/transports/mod.rs), you must implement `bind()` on `CustomTransport`, and `watch_local_addrs()`, `create_sender()`, and `poll_recv()` on `CustomEndpoint`. Additionally, the `CustomSender` returned by `create_sender()` must implement `poll_send()` to handle packet transmission.