How to Configure Custom Transports in iroh with unstable-custom-transports
Enable the unstable-custom-transports Cargo feature to unlock CustomTransport, CustomEndpoint, and CustomSender, then inject your implementation into the Endpoint builder and drive path selection with a custom PathSelector.
The n0-computer/iroh networking stack supports pluggable transports through an unstable feature flag that exposes low-level transport abstractions. By enabling unstable-custom-transports, you can configure custom transports in iroh to route packets over user-defined networks alongside or in place of the built-in IPv4, IPv6, and relay paths.
How Custom Transports Work in iroh
Iroh identifies every networking strategy with a u16 transport-id. The canonical registry lives in [TRANSPORTS.md](https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md), which reserves identifiers such as 0x20 for the internal test transport. When the unstable-custom-transports feature is active, [iroh/src/endpoint.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) re-exports the custom transport API from the socket layer.
Lines 29-48 define the transports sub-module, and lines 41-46 pull in the structs you must implement:
CustomTransport— the trait your networking logic implements.CustomEndpoint— represents the local side of a custom transport.CustomSender— the handle for sending and receiving raw bytes.
Implementing a Custom Transport
A custom transport must provide local addressing and outbound connection capabilities. The reference implementation, TestTransport, lives in iroh/src/test_utils/test_transport.rs and demonstrates the required interface.
The transport returns a CustomAddr via its addr() method. This address contains the transport-id and opaque address data. When the Endpoint needs to send data, it calls connect(), which yields a CustomSender that moves raw packets across your underlying network. The low-level definitions for these types reside in iroh/src/socket/transports.rs, which exposes the TransportConfig enum and the Addr and CustomAddr wrappers.
Configuring the Endpoint Builder
Pass your transport to Endpoint::builder() to include it in the local endpoint. The example in iroh/examples/custom-transport.rs demonstrates the full pattern. You inject the transport with .preset() and strip default paths with .clear_ip_transports() and .clear_relay_transports().
let mut builder = Endpoint::builder(presets::N0)
.secret_key(secret_key)
.preset(transport) // inject the custom transport
.path_selector(Arc::new(PreferTestTransport));
if !keep_ip {
builder = builder.clear_ip_transports();
}
if !keep_relay {
builder = builder.clear_relay_transports();
}
let endpoint = builder.bind().await?;
After binding, the Endpoint holds your custom transport in its internal Vec<TransportConfig> alongside any remaining default transports.
Driving Path Selection with PathSelector
By default, iroh ranks paths by latency. To force traffic over your custom transport, implement PathSelector and attach it to the builder. The example defines PreferTestTransport to prefer any path whose remote address is Addr::Custom with the test transport id, falling back to the lowest-RTT path otherwise.
impl PathSelector for PreferTestTransport {
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
let mut selection = PathSelection::none();
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;
}
// fallback to lowest-RTT path
if let Some(p) = ctx.paths().min_by_key(|p| p.rtt().unwrap_or(Duration::MAX)) {
selection.set(&p);
}
selection
}
}
The PathSelectionContext exposes candidate paths via paths(). Each path exposes its remote address through network_path().remote(). Matching on Addr::Custom lets you prioritize your transport while preserving fallback behavior.
Minimal Runnable Setup
The following excerpt combines the builder configuration and selector into a single setup function. It assumes you have a SecretKey and an Arc<dyn CustomTransport> already initialized.
use iroh::{
Endpoint, SecretKey,
endpoint::{Builder, PathSelector, PathSelection, PathSelectionContext},
transports::{Addr, CustomTransport},
};
use std::sync::Arc;
use std::time::Duration;
const TEST_TRANSPORT_ID: u16 = 0x20;
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;
}
if let Some(p) = ctx.paths().min_by_key(|p| p.rtt().unwrap_or(Duration::MAX)) {
sel.set(&p);
}
sel
}
}
fn builder_with_custom(secret: SecretKey, transport: Arc<dyn CustomTransport>) -> Builder {
Endpoint::builder(presets::N0)
.secret_key(secret)
.preset(transport)
.path_selector(Arc::new(PreferTestTransport))
.clear_ip_transports()
.clear_relay_transports()
}
After calling .bind().await, invoke endpoint.connect() to establish a connection. The discovery layer resolves the custom address, and your selector ensures the custom path wins.
Summary
- Enable
unstable-custom-transportsinCargo.tomlto exposeCustomTransport,CustomEndpoint, andCustomSenderfromiroh/src/endpoint.rs. - Reserve a transport-id in
TRANSPORTS.mdso the stack can identify your custom addresses. - Implement
CustomTransportto provide local addresses and outbound connection handles viaCustomSender. - Attach the transport to an
Endpointbuilder with.preset()and optionally remove built-in transports with.clear_ip_transports()and.clear_relay_transports(). - Control routing by implementing
PathSelectorand matching onAddr::Custominsideselect()to prioritize your transport.
Frequently Asked Questions
What Cargo feature enables custom transports in iroh?
The unstable-custom-transports feature unlocks the custom transport API. When enabled, iroh/src/endpoint.rs re-exports CustomTransport, CustomEndpoint, and CustomSender from the underlying socket layer, allowing you to inject user-defined networking logic into the Endpoint builder.
How does iroh identify a custom transport?
Each custom transport is identified by a unique u16 transport-id defined in TRANSPORTS.md. Addresses carry this id inside CustomAddr, and the PathSelector inspects Addr::Custom to distinguish custom paths from standard IP or relay paths.
Can custom transports replace IP and relay transports entirely?
Yes. The Endpoint builder provides clear_ip_transports() and clear_relay_transports() to remove the default networking paths. If you clear these and supply only a custom transport via .preset(), iroh will route all traffic exclusively through your implementation.
Where can I find a working example of a custom transport?
The official example in iroh/examples/custom-transport.rs demonstrates an end-to-end setup using TestTransport from iroh/src/test_utils/test_transport.rs. It configures the builder, attaches a PreferTestTransport selector, and verifies that the active path uses the custom transport id.
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 →