How to Implement Custom Transport Layers in iroh Using the Unstable Custom Transports Feature
Enable the unstable-custom-transports Cargo feature to extend iroh's networking stack with custom transports by implementing the CustomTransport trait, registering a unique transport ID, and using a custom PathSelector to prioritize your transport during path selection.
Iroh provides a pluggable networking architecture that allows developers to integrate alternative packet transport mechanisms alongside built-in IPv4/IPv6 and relay protocols. By enabling the unstable-custom-transports feature, you can implement custom transport layers that work within iroh's connection establishment and path selection logic, supporting use cases from Bluetooth overlays to Tor tunnels.
Enabling the Unstable Feature
Custom transports are gated behind the unstable-custom-transports Cargo feature flag. Add the following to your Cargo.toml to access the transport extension APIs:
[dependencies]
iroh = { git = "https://github.com/n0-computer/iroh", features = ["unstable-custom-transports"] }
When enabled, the Endpoint module re-exports the necessary types from the internal socket layer. In [iroh/src/endpoint.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs#L29-L48), the transports sub-module exposes CustomTransport, CustomEndpoint, and CustomSender (lines 41-46).
Understanding the Transport Architecture
A custom transport in iroh is identified by a transport ID (a u16 value). The project maintains a canonical registry in [TRANSPORTS.md](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md#L5-L9), where 0x20 is reserved for the internal test transport.
The architecture revolves around three core abstractions defined in the socket layer:
CustomTransport– The trait you implement to define packet sending/receiving logicCustomAddr– A structured address containing the transport ID and opaque endpoint-specific dataPathSelector– The mechanism that chooses which transport path to use for a given connection
Implementing the CustomTransport Trait
Your transport implementation must satisfy the CustomTransport interface expected by the socket layer. While the internal trait definition resides in the socket transports module, you can reference the test implementation in [iroh/src/test_utils/test_transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs) as a working template.
A minimal implementation requires:
- Address generation – Construct a
CustomAddrcontaining your transport ID and endpoint-specific metadata - Connection establishment – Return a
CustomSendercapable of sending and receiving raw bytes - Transport identification – Return your allocated transport ID when queried
Configuring the Endpoint Builder
To activate your custom transport, inject it during Endpoint construction using the builder API. The example in [iroh/examples/custom-transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) demonstrates this pattern in the configure method (lines 71-87):
use iroh::{Endpoint, endpoint::Builder, SecretKey};
use std::sync::Arc;
fn builder_with_custom(
secret_key: SecretKey,
transport: Arc<dyn CustomTransport>,
) -> Builder {
Endpoint::builder(presets::N0)
.secret_key(secret_key)
.preset(transport) // Inject the custom transport
.clear_ip_transports() // Optional: disable IPv4/IPv6
.clear_relay_transports() // Optional: disable relay fallback
}
The .preset() method adds your transport to the internal Vec<TransportConfig> maintained by the builder, while clear_ip_transports() and clear_relay_transports() allow you to force exclusive use of your custom implementation.
Creating a Custom Path Selector
By default, iroh uses RTT-based path selection. To prioritize your custom transport, implement the PathSelector trait and inspect the PathSelectionContext for custom addresses. The reference implementation in [iroh/examples/custom-transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs#L37-L69) shows a selector that prefers the test transport:
use iroh::endpoint::{PathSelector, PathSelection, PathSelectionContext, transports::Addr};
const TEST_TRANSPORT_ID: u16 = 0x20;
struct PreferTestTransport;
impl PathSelector for PreferTestTransport {
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
let mut selection = PathSelection::none();
// Prioritize paths using our custom transport
if let Some(path) = ctx.paths().find(|p| {
matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID)
}) {
selection.set(&path);
return selection;
}
// Fallback to lowest RTT path
for path in ctx.paths() {
if selection.is_none() || path.rtt() < selection.rtt() {
selection.set(&path);
}
}
selection
}
}
Attach the selector to your builder using .path_selector(Arc::new(PreferTestTransport)).
Complete Working Example
Putting the components together, here is a minimal example that establishes a connection using only the custom test transport:
use iroh::{Endpoint, SecretKey, endpoint::PathSelectionContext};
use iroh::endpoint::{PathSelector, PathSelection, transports::Addr};
use std::sync::Arc;
// Assume TestTransport implements CustomTransport
use iroh::test_utils::test_transport::{TestTransport, TEST_TRANSPORT_ID};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let secret = SecretKey::generate();
let transport = Arc::new(TestTransport::new());
// Build endpoint with custom transport
let endpoint = Endpoint::builder(presets::N0)
.secret_key(secret)
.preset(transport)
.path_selector(Arc::new(PreferTestTransport))
.clear_ip_transports()
.clear_relay_transports()
.bind()
.await?;
// Connect using the custom transport path
let conn = endpoint.connect(node_id, ALPN).await?;
Ok(())
}
This configuration forces the endpoint to use only your custom transport implementation, bypassing standard networking paths entirely.
Key Source Files
Understanding the following source locations is essential for implementing custom transports:
- [
iroh/src/endpoint.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) – Public API surface (Endpoint,Builder) and re-exports ofCustomTransporttypes (lines 29-48) - [
iroh/src/socket/transports.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) – Low-level transport abstractions includingTransportConfig,Addr, andCustomAddr - [
iroh/src/test_utils/test_transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/test_utils/test_transport.rs) – Reference implementation showing theCustomTransporttrait contract - [
iroh/examples/custom-transport.rs](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) – End-to-end CLI example demonstrating transport selection and configuration (lines 37-69 for selector, lines 71-87 for builder) - [
TRANSPORTS.md](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md) – Official registry of transport IDs and reserved ranges
Summary
- Enable the feature flag
unstable-custom-transportsin yourCargo.tomlto access extension APIs - Implement
CustomTransportto define packet transport logic and address formats - Register a transport ID in
TRANSPORTS.mdto avoid collisions with other implementations - Use the builder API to inject your transport via
.preset()and optionally disable built-in transports with.clear_ip_transports()and.clear_relay_transports() - Implement
PathSelectorto prioritize your custom transport over IPv4/IPv6 paths during connection establishment - Reference the test implementation in
test_transport.rsand the working example incustom-transport.rsfor concrete trait implementations
Frequently Asked Questions
What does the unstable-custom-transports feature enable?
The feature unlocks the CustomTransport trait and associated types in the public API, allowing you to inject arbitrary packet transport mechanisms into iroh's networking stack. Without this flag, the transport extension points are hidden and the necessary builder methods are unavailable.
How do I reserve a transport ID for my implementation?
Transport IDs are 16-bit unsigned integers (u16) tracked in the [TRANSPORTS.md](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md) file at the repository root. To avoid collisions, submit a pull request that registers your ID with a description of your transport mechanism. The range 0x0001-0x00FF is reserved for experimental use, while 0x20 is allocated for the internal test transport.
Can I use multiple custom transports simultaneously?
Yes. The Endpoint builder accepts multiple transports via repeated calls to .preset(). Each transport must have a unique ID. The PathSelector implementation determines which transport wins for a given connection by inspecting the Addr::Custom variant in the path selection context and choosing based on your priority logic.
What is the difference between CustomEndpoint and CustomSender?
CustomEndpoint represents the local binding of your transport (analogous to a socket), while CustomSender is the per-connection handle returned by your transport's connect method that actually transmits and receives byte streams. The endpoint manages the local address and incoming connection state, whereas the sender handles the active data flow for established connections.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →