# How to Implement Custom Transport Protocols with Iroh's unstable-custom-transports Feature

> Learn to implement custom transport protocols in Iroh by enabling unstable-custom-transports and implementing key traits. Route packets with your own protocol for enhanced control.

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

---

**Enable the `unstable-custom-transports` Cargo feature, implement the `CustomTransport`, `CustomEndpoint`, and `CustomSender` traits, and register your transport via `Builder::add_custom_transport` to route packets through your custom protocol.**

Iroh's networking stack extends beyond QUIC to support user-defined packet transport mechanisms. By enabling the **unstable-custom-transports** feature in the `iroh` crate, you can inject bespoke protocols—such as specialized radio links, VPN tunnels, or simulation layers—directly into the connection lifecycle.

## Enabling the Feature Flag

To compile with custom transport support, add the feature to your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml):

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

```

This activates 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 exposes the `Builder::add_custom_transport` method in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) at lines 800-814.

## Understanding the Custom Transport Architecture

The architecture revolves around three core traits that define how raw packets flow through your custom implementation.

### The Three Core Traits

Located in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs), these traits form the contract between your code and Iroh's path selection machinery:

- **`CustomTransport`**: The entry point trait. Its `bind()` method returns a `Box<dyn CustomEndpoint>` and is called once during endpoint construction.
- **`CustomEndpoint`**: Represents a bound transport instance. It manages `watch_local_addrs()` for address advertisement, `poll_recv()` for inbound packet processing, and `create_sender()` for outbound transmission handles.
- **`CustomSender`**: Handles actual packet transmission via `poll_send()` and validates destination addresses with `is_valid_send_addr()`.

### Lifecycle and Integration

When you call `Builder::add_custom_transport`, the builder stores an `Arc<dyn CustomTransport>` inside its internal `transports` vector. During `Builder::bind()`, the runtime invokes `CustomTransport::bind()` to create the endpoint. The runtime then polls `CustomEndpoint::poll_recv` to inject incoming packets into the path-selection logic via `RecvInfo` structures. For outbound traffic, `create_sender` provides an `Arc<dyn CustomSender>` that the runtime invokes when the path selector chooses your transport over IP or relay paths.

## Implementing a Minimal Custom Transport

The following example demonstrates a basic echo transport that implements all required traits. This pattern references the trait definitions 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_base::CustomAddr;
use iroh::endpoint::transports::custom::{CustomTransport, CustomEndpoint, CustomSender};

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>> {
        // Advertise a single dummy address for discovery
        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>> {
        // Return 0 packets in this toy example
        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<()>> {
        // Drop packets in this example implementation
        Poll::Ready(Ok(()))
    }
}

```

## Registering Your Transport with the Endpoint

To activate your implementation, wrap it in an `Arc` and register it during endpoint construction:

```rust
let my_transport = Arc::new(EchoTransport);
let endpoint = Endpoint::builder(presets::N0)
    .add_custom_transport(my_transport)
    .bind()
    .await?;

```

As implemented in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), `add_custom_transport` stores your transport configuration. When the endpoint binds, it automatically invokes your `bind()` method and integrates the resulting endpoint into the runtime's packet polling loop.

## Path Selection and Production Patterns

By default, Iroh uses an internal path selector that prioritizes low-latency IP or relay paths. To prefer your custom transport, implement a `PathSelector` and attach it via `Builder::path_selector`. The repository's example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) demonstrates a `PreferTestTransport` selector that chooses custom addresses when available, falling back to standard paths otherwise.

For a complete reference implementation, examine the test transport in [`iroh/src/test_utils.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils.rs). This implementation provides a full in-memory network simulation using `TestTransport` and `TestNetwork`, showing how to handle `CustomAddr` serialization, packet buffering, and async runtime integration.

## Summary

- Enable the `unstable-custom-transports` feature in your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) to access the custom transport API.
- Implement `CustomTransport`, `CustomEndpoint`, and `CustomSender` from [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs) to define your protocol's behavior.
- Register your transport via `Builder::add_custom_transport` before calling `bind()`.
- Use `watch_local_addrs()` to advertise custom addresses and `poll_recv()` to handle inbound packets from the network.
- Optional: Provide a custom `PathSelector` to prioritize your transport over IP or relay paths when establishing connections.

## Frequently Asked Questions

### Is the unstable-custom-transports feature production-ready?

No. As indicated by the "unstable" prefix, this feature is not covered by semantic versioning guarantees. The API may change between minor releases without a major version bump, making it suitable for experimentation and controlled deployments but not for stable production systems requiring long-term API stability.

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

The `PathSelector` implementation evaluates all available paths—including IP, relay, and custom addresses—for each remote peer. When your custom transport advertises addresses via `watch_local_addrs`, the selector can choose them based on your priority logic. If you do not provide a custom selector, Iroh defaults to standard latency-based selection, which may bypass your transport if IP paths offer lower round-trip times.

### Can I use multiple custom transports simultaneously?

Yes. The `Builder` supports multiple transports via repeated calls to `add_custom_transport`. Each transport is stored in the internal `transports` vector and bound during `Builder::bind()`. The path selector then chooses between all available transport options based on your selection criteria and the remote peer's advertised addresses.

### Where can I find a complete working example?

The `iroh` repository includes a working demonstration in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs). This example wires a `TestTransport` from [`iroh/src/test_utils.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils.rs), implements a custom path selector, and verifies that connections route through the custom transport layer. It serves as the definitive reference for implementing custom transports in real-world scenarios.