How to Debug iroh Connection Issues Using Path Events and Connection State Inspection
Use conn.path_events() to monitor real-time path changes and conn.paths() to inspect the current snapshot of active routes, allowing you to trace exactly why a QUIC connection selects, drops, or stalls on a specific network path.
The iroh crate from the n0-computer/iroh repository maintains a live view of every candidate path a QUIC connection can use. When packets stop flowing or a relay is selected unexpectedly, you need visibility into that routing state. Learning how to debug iroh connection issues using path events and connection state inspection gives you deterministic tools to observe PathEvent emissions and snapshot the active path list at any moment.
The Two APIs: path_events() and paths()
iroh exposes two complementary debugging surfaces on its Connection object. Together they let you watch path history in real time and capture the current routing picture on demand.
-
conn.path_events()returns an asynchronousPathEventStream. As implemented iniroh/src/endpoint/connection.rs, this stream yieldsPathEventvalues whenever the path-selection subsystem adds a route, removes a route, or promotes a different route to active. Use it to watch NAT traversal, relay fallback, and link-speed changes as they happen. -
conn.paths()returns aPathListsnapshot that implementsIterator<Item = &PathInfo>. Call it at any instant to collect per-path diagnostics such asremote_addr(),rtt(),is_selected(), andis_relay(). This call is cheap, non-blocking, and read-only.
How Path State Is Managed Inside the Connection Struct
Inside iroh/src/endpoint/connection.rs at line 447, the Connection struct stores a field named paths: PathStateReceiver. That receiver forwards updates from the path-selection subsystem to any listener attached via path_events().
The PathEvent enum, defined around line 1017 in iroh/src/socket/transports.rs, carries three variants:
PathEvent::Added(PathInfo)— a new candidate path was discovered. It may be direct, relay-based, or custom.PathEvent::Removed(PathId)— the path timed out or was discarded by the selector.PathEvent::Selected(PathId)— a different path was promoted to the active route for the connection.
When Connection::paths() is invoked, it materializes a PathList from the same underlying state. Each entry is a PathInfo that lets you correlate a PathId with concrete socket addresses and performance metrics. The default selector that drives these updates is BiasedRttPathSelector, located in iroh/src/socket/transports/biased_rtt_path_selector.rs.
Typical Troubleshooting Workflow for iroh Connections
Match your symptom to the event stream and snapshot data:
-
Connection stalls after handshake. Check whether
PathEvent::Selectedfires after the initial path probe. Ifconn.paths()shows every route with an infinite RTT, the transport has not validated any candidate. Verify that at least one path reports a finite RTT before concluding the connection is dead. -
Traffic flows only through a relay. Watch for
PathEvent::Addedentries wherePathInfo::is_direct()is true but never become selected. TheBiasedRttPathSelectormay filter them out if they are considered unreliable. Log each added path and its flags to confirm whether a direct candidate exists. -
Path flaps between two IP addresses. Repeated
PathEvent::Selectedvariants with different IDs indicate oscillation. Compare the RTT values of the two paths inconn.paths(). If the RTTs are close, the selector may be bouncing between near-equal options. You can increase hysteresis or swap the selector to isolate the behavior. -
No path events appear at all. If the stream stays silent, the transport likely failed to discover any candidate. Confirm the underlying socket can send and receive UDP packets. Firewall rules that block STUN or QUIC traffic prevent the
PathStateReceiverfrom ever emitting an event.
Code Examples for Path Event Debugging
The following snippets show how to instrument an iroh client or server with path-level logging.
Watch path events and log the selected path
let mut events = conn.path_events();
while let Some(event) = events.next().await {
match event {
PathEvent::Added(p) => {
debug!("new path [{:?}] {}", p.id(), p.remote_addr());
}
PathEvent::Removed(id) => {
debug!("path removed [{:?}]", id);
}
PathEvent::Selected(id) => {
let selected = conn
.paths()
.iter()
.find(|p| p.id() == id)
.expect("selected path must exist");
info!(
"selected path switched to [{:?}] {} (rtt={:?})",
selected.id(),
selected.remote_addr(),
selected.rtt()
);
}
}
}
One-off inspection of the current path list
let paths = conn.paths();
for p in paths.iter() {
println!(
"Path {} – {} – rtt={:?} – selected={} – relay={}",
p.id(),
p.remote_addr(),
p.rtt(),
p.is_selected(),
p.is_relay()
);
}
Wait for a direct path selection
conn.wait_selected(Duration::from_secs(10), |p| p.remote_addr().is_ip())
.await
.expect("no direct path selected within timeout");
The wait_selected helper is available in the test utilities. You can reproduce the same logic by combining path_events() with a manual paths() check inside a timeout loop.
Key Source Files for Deep Dives
For deeper inspection of the implementation, study these files in the n0-computer/iroh repository:
iroh/src/endpoint/connection.rs— DefinesConnection, stores thePathStateReceiverat line 447, and implementspath_events()andpaths()between lines 447 and 452.iroh/src/socket/transports.rs— Contains thePathEventenum andPathInfostructure around line 1017.iroh/src/socket/transports/biased_rtt_path_selector.rs— Implements the default selector that balances RTT against relay fallback.iroh/src/socket/transports/relay/actor.rs— Shows how relay paths are created and marked, which is critical when debugging unexpected relay selections.iroh/tests/patchbay/util.rs— Demonstrates concrete test logging of the selected path usingconn.path_events()at lines 429–432.
Summary
conn.path_events()yields a livePathEventStreamof added, removed, and selected path events for the full connection lifetime.conn.paths()provides a cheap, read-onlyPathListsnapshot with per-route RTT, address, and selection status.- The
Connectionstruct holds path state in aPathStateReceiverfield insideiroh/src/endpoint/connection.rs. - Use event streaming plus snapshot cross-referencing to diagnose stalls, relay fallback, and path oscillation deterministically.
Frequently Asked Questions
What is the difference between path_events() and paths() in iroh?
path_events() returns an asynchronous stream that emits PathEvent values every time the path state changes. paths() returns an immediate snapshot of the current PathList. You should use the stream for real-time tracing and the snapshot for point-in-time diagnostics.
Why does my iroh connection only use a relay path?
If direct paths never receive a PathEvent::Selected promotion, the BiasedRttPathSelector may consider them unreliable or unreachable. Log every PathEvent::Added event and inspect is_direct() on the resulting PathInfo. If direct candidates exist but are ignored, check RTT and firewall conditions.
How do I detect path flapping or oscillation in iroh?
Monitor path_events() for rapid sequences of PathEvent::Selected with different PathId values. When flapping occurs, call conn.paths() to compare RTTs. If two paths have nearly identical RTTs, the default selector may alternate between them.
Where does iroh store path state inside the Connection object?
The Connection struct stores path updates in a paths: PathStateReceiver field at line 447 of iroh/src/endpoint/connection.rs. That receiver bridges the path-selection subsystem and the public path_events() and paths() APIs.
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 →