# When to Use Custom Transports in Iroh: 5 Scenarios and Implementation Guide

> Discover when to use custom transports in Iroh for Bluetooth, Tor, hardware offloading, and deterministic testing. Learn to extend Iroh's capabilities beyond IP networks.

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

---

**Use custom transports in iroh when you need to operate over non-IP networks like Bluetooth or Tor, require specialized hardware offloading, or need deterministic testing environments while retaining iroh's high-level endpoint and connection management.**

The n0-computer/iroh networking stack abstracts packet-level I/O behind modular transports. While the default configuration uses UDP-based QUIC with optional relay support for NAT traversal, implementing custom transports in iroh allows you to integrate alternative packet-level mediums such as Bluetooth Low Energy, Tor, or proprietary radio links.

## When to Use Custom Transports in Iroh

The iroh codebase provides specific extension points for custom transports through the `unstable-custom-transport` feature flag. Consider implementing a custom transport when you encounter these five scenarios:

- **Non-IP environments**: Bluetooth Low Energy, LoRa, or other radio links that cannot use the built-in UDP stack. Custom transports expose a `CustomAddr` type for these links.

- **Privacy-oriented routing**: Tor or I2P tunnels where you want the transport to handle its own encryption and obfuscation. Iroh treats the entire tunnel as a black-box address.

- **Performance-specific needs**: Zero-copy batch sending (GSO) or hardware offloading that requires implementing `max_transmit_segments` to enable larger batch sizes than the regular UDP transport provides.

- **Testing and simulation**: In-memory mock networks that replace physical networking with deterministic channels, as demonstrated in the test utilities.

- **Special address semantics**: When you need to attach extra metadata to addresses, such as device IDs or encryption keys. `CustomAddr` can carry arbitrary opaque data, and the transport exposes this via `RecvInfo`.

When you add a custom transport, you also gain access to a **path selector** that can prioritize custom paths over standard IP paths. This is essential when the custom path offers lower latency, higher reliability, or stronger security guarantees.

## Architecture of Custom Transports

The custom transport system in iroh follows a trait-based architecture that integrates with the endpoint builder and path selection logic.

### Transport Registration and Builder API

Custom transports are registered through the endpoint builder using `add_custom_transport`, which is gated behind the `unstable-custom-transport` feature. In [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), this method stores a `TransportConfig::Custom` inside the endpoint configuration:

```rust
#[cfg(feature = "unstable-custom-transports")]
pub fn add_custom_transport(mut self, factory: Arc<dyn CustomTransport>) -> Self {
    self.transports.push(TransportConfig::Custom(factory));
    self
}

```

This registration pattern allows the endpoint to instantiate your transport during the binding phase.

### Core Traits: CustomTransport and CustomEndpoint

A custom transport implements the `CustomTransport` trait, which creates a `CustomEndpoint` instance. The endpoint provides a local address watcher, a sender factory, and a poll-based receive loop. The trait definitions in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs) specify:

```rust
pub trait CustomTransport: std::fmt::Debug + Send + Sync + 'static {
    fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>>;
}

pub trait CustomEndpoint: std::fmt::Debug + Send + Sync + 'static {
    fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>>;
    fn create_sender(&self) -> Arc<dyn CustomSender>;
    fn poll_recv(&mut self, …) -> Poll<io::Result<usize>>;
    fn max_transmit_segments(&self) -> NonZeroUsize { … }
}

```

### Address Representation with CustomAddr

Custom transports use `CustomAddr` to represent network addresses. Defined in [`iroh-base/src/endpoint_addr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/endpoint_addr.rs), this struct embeds a transport ID (registered in [`TRANSPORTS.md`](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md)) and opaque address bytes:

```rust
pub struct CustomAddr { /* … */ }

```

This design allows custom transports to carry arbitrary addressing information while maintaining type safety within the iroh networking stack.

### Path Selection and Prioritization

The default path selector prefers IPv6, then IPv4, then relays. By supplying your own `PathSelector` implementation, you can force the custom transport to win whenever a viable path exists. The example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) demonstrates a `PreferTestTransport` selector:

```rust
impl PathSelector for PreferTestTransport {
    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() == TEST_TRANSPORT_ID)
        ) {
            selection.set(&p);
            return selection;
        }
        // fall back to lowest‑RTT
        …
    }
}

```

### Performance Optimization with Segment Offloading

Custom transports can implement `max_transmit_segments` to expose kernel-level GSO (Generic Segmentation Offload) capabilities. The default implementation in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs) returns `NonZeroUsize::MIN`, but overriding this allows higher throughput through larger batch sizes:

```rust
fn max_transmit_segments(&self) -> NonZeroUsize { NonZeroUsize::MIN }

```

## Implementation Example

Below is a minimal implementation that plugs in a dummy in-memory transport. This example uses the test utilities from [`iroh/src/test_utils/test_transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs) and demonstrates custom path selection:

```rust
use std::{sync::Arc, time::Duration};
use iroh::{
    Endpoint, SecretKey, TransportAddr,
    endpoint::{
        Builder, Connection, presets,
        transports::{Addr, PathSelection, PathSelectionContext, PathSelector},
    },
    protocol::{AcceptError, ProtocolHandler, Router},
    test_utils::test_transport::{TEST_TRANSPORT_ID, TestNetwork, TestTransport},
};
use n0_error::Result;

/// Prefer the test custom transport above all others.
#[derive(Debug)]
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;
        }
        // fallback to the lowest‑RTT path
        if let Some(p) = ctx.paths()
            .filter_map(|p| p.stats().map(|s| (p, s.rtt)))
            .min_by_key(|(_, rtt)| *rtt)
            .map(|(p, _)| p)
        {
            sel.set(&p);
        }
        sel
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    // Create a simulated network that supplies the custom transport.
    let net = TestNetwork::new();
    let secret_a = SecretKey::from([0u8; 32]);
    let secret_b = SecretKey::from([1u8; 32]);

    // Build two endpoints, each equipped with the test transport.
    let t_a = net.create_transport(secret_a.public())?;
    let ep_a = Endpoint::builder(presets::N0)
        .secret_key(secret_a.clone())
        .preset(t_a)
        .path_selector(Arc::new(PreferTestTransport))
        .bind()
        .await?;

    let t_b = net.create_transport(secret_b.public())?;
    let ep_b = Endpoint::builder(presets::N0)
        .secret_key(secret_b.clone())
        .preset(t_b)
        .path_selector(Arc::new(PreferTestTransport))
        .bind()
        .await?;

    // Simple echo protocol.
    #[derive(Debug, Clone)]
    struct Echo;
    impl ProtocolHandler for Echo {
        async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
            let (mut send, mut recv) = conn.accept_bi().await?;
            tokio::io::copy(&mut recv, &mut send).await?;
            send.finish()?;
            Ok(())
        }
    }

    // Server side
    let _router = Router::builder(ep_b).accept(b"iroh-example/echo/0", Echo).spawn();

    // Client side – connect using only the remote endpoint id.
    let conn = ep_a.connect(secret_b.public(), b"iroh-example/echo/0").await?;

    // Verify that the selected path is our custom transport.
    let selected = conn.paths().iter().find(|p| p.is_selected()).unwrap();
    assert!(matches!(selected.remote_addr(),
        TransportAddr::Custom(c) if c.id() == TEST_TRANSPORT_ID));

    // Perform a round‑trip.
    let (mut send, mut recv) = conn.open_bi().await?;
    send.write_all(b"hello").await?;
    send.finish().await?;
    let mut buf = Vec::new();
    recv.read_to_end(&mut buf).await?;
    assert_eq!(buf, b"hello");
    Ok(())
}

```

Custom addresses are advertised via the address-lookup service. The test transport registers its address in `TestAddrLookup::resolve`, ensuring peers can discover the custom path just like any other transport:

```rust
TransportAddr::Custom(CustomAddr::from_parts(TEST_TRANSPORT_ID, endpoint_id.as_bytes()))

```

## Summary

- Custom transports in iroh enable non-IP networking through the `unstable-custom-transport` feature flag and the `add_custom_transport` method.
- Implement `CustomTransport` and `CustomEndpoint` from [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs) to create alternative packet-level mediums.
- Use `CustomAddr` for transport-specific addressing with arbitrary metadata.
- Override `max_transmit_segments` to enable hardware offloading and GSO for performance-critical applications.
- Provide a custom `PathSelector` to prioritize custom transports over default IP paths based on latency, reliability, or security requirements.

## Frequently Asked Questions

### How do I enable custom transport support in iroh?

Enable the `unstable-custom-transports` feature in your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) when depending on iroh. This exposes the `add_custom_transport` method on the endpoint builder and the necessary trait definitions in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs).

### What is the difference between a custom transport and the default QUIC transport?

The default transport uses UDP-based QUIC with optional relay support for NAT traversal. A custom transport replaces the underlying packet I/O layer entirely, allowing you to use Bluetooth, Tor, in-memory channels, or proprietary hardware while retaining iroh's high-level connection management, encryption, and protocol handling.

### Can I use multiple custom transports simultaneously?

Yes. The endpoint builder accepts multiple transports, and you can register several custom transports alongside the default IP transport. Use a custom `PathSelector` implementation to prioritize between them based on availability, latency, or specific transport IDs.

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

The default selector prefers IPv6, then IPv4, then relays. When you implement a custom `PathSelector`, you can inspect the `PathSelectionContext` to find paths using your custom transport via `Addr::Custom` matching, and force selection of those paths when they provide better connectivity or security characteristics than standard IP routes.