# How to Configure Custom Transports in iroh for Specialized Network Paths

> Learn to configure custom transports in iroh for specialized network paths like Bluetooth or LoRa. Implement traits and control path selection for enhanced connectivity.

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

---

**You can configure custom transports in iroh by implementing the `CustomTransport`, `CustomEndpoint`, and `CustomSender` traits, registering them via `Endpoint::builder().add_custom_transport()`, and optionally controlling path selection with a custom `PathSelector` to prioritize specialized links like Bluetooth, LoRa, or Unix domain sockets.**

The iroh networking stack supports pluggable transport abstractions that allow you to route QUIC traffic over non-standard network paths. While the library provides built-in transports for IP/UDP and relay connections, you can configure custom transports in iroh to leverage specialized hardware or logical channels. This guide walks through the trait implementations and builder configuration required to integrate alternative transports, referencing the actual source code in `n0-computer/iroh`.

## Enable the Unstable Feature Flag

Custom transport support is currently gated behind the `unstable-custom-transports` Cargo feature because the API surface is still evolving. Add the feature to your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) before implementing any transport traits.

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

```

## Implement the Core Transport Traits

The transport system is built around three core traits defined in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs). You must provide concrete implementations for each layer to create a functional transport.

### CustomTransport: The Factory

The `CustomTransport` trait acts as a factory that produces endpoint instances. Implement the `bind()` method to initialize your underlying hardware or OS resources (e.g., opening a Bluetooth socket or LoRa radio).

```rust
use std::io;
use std::sync::Arc;
use iroh_base::CustomAddr;
use iroh::socket::transports::{CustomTransport, CustomEndpoint};

#[derive(Debug)]
struct MyTransport;

impl CustomTransport for MyTransport {
    fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
        // Initialize hardware resources here
        Ok(Box::new(MyEndpoint::new()?))
    }
}

```

### CustomEndpoint: The Runtime Interface

The `CustomEndpoint` trait defined in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs) handles address discovery and packet reception. You must implement four key methods:

- **`watch_local_addrs()`**: Returns the set of local addresses this transport exposes to the iroh endpoint
- **`poll_recv()`**: The async polling method that pulls packets from the underlying socket into iroh's buffers
- **`create_sender()`**: Returns a `CustomSender` instance for transmitting packets
- **`max_transmit_segments()`**: Indicates GSO (Generic Segmentation Offload) support level

```rust
use std::task::{Context, Poll};
use std::io::IoSliceMut;
use iroh::socket::transports::{CustomEndpoint, CustomSender, RecvInfo};

#[derive(Debug)]
struct MyEndpoint {
    // Internal state, e.g., socket handle
}

impl MyEndpoint {
    fn new() -> io::Result<Self> {
        Ok(Self { /* ... */ })
    }
}

impl CustomEndpoint for MyEndpoint {
    fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
        // Return local custom addresses available on this endpoint
        n0_watcher::Direct::new(vec![
            CustomAddr::new(0x42, vec![0x01, 0x02])
        ])
    }

    fn create_sender(&self) -> Arc<dyn CustomSender> {
        Arc::new(MySender { /* ... */ })
    }

    fn poll_recv(
        &mut self,
        cx: &mut Context,
        bufs: &mut [IoSliceMut<'_>],
        metas: &mut [noq_udp::RecvMeta],
        recv_infos: &mut [RecvInfo],
    ) -> Poll<io::Result<usize>> {
        // Read from underlying socket and populate bufs
        // For each packet, write RecvInfo::new(custom_addr, local_addr_opt)
        unimplemented!()
    }

    fn max_transmit_segments(&self) -> std::num::NonZeroUsize {
        // Return 1 for no GSO support, or higher for batching
        std::num::NonZeroUsize::new(4).unwrap()
    }
}

```

### CustomSender: Packet Transmission

The `CustomSender` trait handles the egress path. Implement `is_valid_send_addr()` to filter which remote addresses your transport can handle, and `poll_send()` to encode and transmit packets to the underlying medium.

```rust
use iroh::socket::transports::{CustomSender, Transmit};

#[derive(Debug)]
struct MySender { /* ... */ }

impl CustomSender for MySender {
    fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
        // Only accept addresses belonging to this transport type
        addr.id() == 0x42
    }

    fn poll_send(
        &self,
        cx: &mut Context,
        dst: &CustomAddr,
        src: Option<&CustomAddr>,
        transmit: &Transmit<'_>,
    ) -> Poll<io::Result<()>> {
        // Encode packet and hand to OS/hardware socket
        unimplemented!()
    }
}

```

## Register the Transport with the Endpoint Builder

Once your traits are implemented, register the transport using the builder API in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs). The `add_custom_transport()` method accepts an `Arc<dyn CustomTransport>` and stores it in the builder's internal transports vector.

You can optionally disable built-in transports to create an endpoint that operates solely over your custom link.

```rust
use iroh::endpoint::{Builder, presets};
use std::sync::Arc;

let builder = Builder::new(presets::N0)
    .add_custom_transport(Arc::new(MyTransport))
    // Optional: remove default transports
    .clear_ip_transports()
    .clear_relay_transports();

```

When `bind()` is called, the builder iterates over all registered transports and invokes `CustomTransport::bind()` to instantiate the endpoints.

## Implement Path Selection Strategy

By default, iroh uses an internal path selection algorithm that prefers IPv6, then IPv4, then relay connections. To prioritize your custom transport, implement the `PathSelector` trait and register it via `path_selector()` on the builder.

The `select()` method in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) receives a `PathSelectionContext` containing all candidate paths and returns the chosen path.

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

struct PreferMyTransport;

impl PathSelector for PreferMyTransport {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        // Prioritize custom transport paths with id 0x42
        if let Some(p) = ctx.paths().find(|p| {
            matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == 0x42)
        }) {
            let mut sel = PathSelection::none();
            sel.set(&p);
            return sel;
        }
        
        // Fallback to lowest RTT path
        ctx.paths()
            .filter_map(|p| p.stats().map(|s| (p, s.rtt)))
            .min_by_key(|(_, rtt)| *rtt)
            .map_or_else(PathSelection::none, |(p, _)| {
                let mut sel = PathSelection::none();
                sel.set(p);
                sel
            })
    }
}

// Apply the selector
let builder = builder.path_selector(Arc::new(PreferMyTransport));

```

## Complete Working Example

Below is a minimal, self-contained example that demonstrates binding a custom transport and configuring the endpoint. This mirrors the official example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) but focuses on the essential configuration points.

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

// Transport implementation
#[derive(Debug)]
struct MyTransport;
impl iroh::socket::transports::CustomTransport for MyTransport {
    fn bind(&self) -> std::io::Result<Box<dyn iroh::socket::transports::CustomEndpoint>> {
        Ok(Box::new(MyEndpoint {}))
    }
}

#[derive(Debug)]
struct MyEndpoint;
impl iroh::socket::transports::CustomEndpoint for MyEndpoint {
    fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
        n0_watcher::Direct::new(vec![CustomAddr::new(0x99, vec![0x10, 0x20])])
    }
    
    fn create_sender(&self) -> Arc<dyn iroh::socket::transports::CustomSender> {
        Arc::new(MySender {})
    }
    
    fn poll_recv(
        &mut self,
        _cx: &mut std::task::Context,
        _bufs: &mut [std::io::IoSliceMut<'_>],
        _metas: &mut [noq_udp::RecvMeta],
        _recv_infos: &mut [iroh::socket::RecvInfo],
    ) -> std::task::Poll<std::io::Result<usize>> {
        std::task::Poll::Pending
    }
    
    fn max_transmit_segments(&self) -> std::num::NonZeroUsize {
        std::num::NonZeroUsize::new(4).unwrap()
    }
}

#[derive(Debug)]
struct MySender;
impl iroh::socket::transports::CustomSender for MySender {
    fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
        addr.id() == 0x99
    }
    
    fn poll_send(
        &self,
        _cx: &mut std::task::Context,
        _dst: &CustomAddr,
        _src: Option<&CustomAddr>,
        _tx: &iroh::socket::transports::Transmit<'_>,
    ) -> std::task::Poll<std::io::Result<()>> {
        std::task::Poll::Ready(Ok(()))
    }
}

// Path selector
#[derive(Debug)]
struct PreferMyTransport;
impl PathSelector for PreferMyTransport {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        if let Some(p) = ctx.paths().find(|p| {
            matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == 0x99)
        }) {
            let mut sel = PathSelection::none();
            sel.set(&p);
            return sel;
        }
        ctx.paths()
            .filter_map(|p| p.stats().map(|s| (p, s.rtt)))
            .min_by_key(|(_, rtt)| *rtt)
            .map_or_else(PathSelection::none, |(p, _)| {
                let mut sel = PathSelection::none();
                sel.set(p);
                sel
            })
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let secret = SecretKey::from([0u8; 32]);
    
    let builder = Endpoint::builder(presets::N0)
        .secret_key(secret)
        .add_custom_transport(Arc::new(MyTransport))
        .clear_ip_transports()
        .clear_relay_transports()
        .path_selector(Arc::new(PreferMyTransport));
    
    let endpoint = builder.bind().await?;
    println!("Local addresses: {:?}", endpoint.addr());
    
    Ok(())
}

```

## Summary

- **Enable the feature**: Add `unstable-custom-transports` to your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) to access the trait definitions in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs).
- **Implement three traits**: Provide `CustomTransport` as a factory, `CustomEndpoint` for receive logic and address discovery, and `CustomSender` for transmit operations.
- **Register with the builder**: Use `add_custom_transport()` on the `Endpoint::builder()` to register your implementation, optionally clearing default IP or relay transports.
- **Control path selection**: Implement `PathSelector` to prioritize custom paths over standard network routes, using `path_selector()` on the builder.
- **Reference the example**: Study [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) for a complete end-to-end implementation including test network simulation.

## Frequently Asked Questions

### What Cargo feature is required to configure custom transports in iroh?

You must enable the `unstable-custom-transports` feature in your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) because the API is not yet stabilized. This feature exposes the `CustomTransport`, `CustomEndpoint`, and `CustomSender` traits in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs).

### How does iroh decide which transport to use for a connection?

Iroh evaluates all available paths (IP, relay, and custom) through the `PathSelector` trait. The default selector prefers IPv6, then IPv4, then relay. By providing a custom `PathSelector` implementation and registering it via `path_selector()`, you can prioritize custom transports based on address type, latency, or other application-specific criteria.

### Can custom transports run alongside standard IP networking?

Yes. You can register custom transports alongside built-in transports by using `add_custom_transport()` without calling `clear_ip_transports()` or `clear_relay_transports()`. The endpoint will advertise all available addresses, and the path selector will choose the appropriate transport for each connection attempt.

### What performance considerations apply to custom transport implementations?

Your `CustomEndpoint` should implement `max_transmit_segments()` to indicate GSO support if your underlying hardware supports batching. The `poll_recv()` and `poll_send()` methods integrate with iroh's async runtime, so they should be non-blocking and return `Poll::Pending` when waiting for I/O to avoid blocking the connection driver.