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 asynchronous PathEventStream. As implemented in iroh/src/endpoint/connection.rs, this stream yields PathEvent values 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 a PathList snapshot that implements Iterator<Item = &PathInfo>. Call it at any instant to collect per-path diagnostics such as remote_addr(), rtt(), is_selected(), and is_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::Selected fires after the initial path probe. If conn.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::Added entries where PathInfo::is_direct() is true but never become selected. The BiasedRttPathSelector may 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::Selected variants with different IDs indicate oscillation. Compare the RTT values of the two paths in conn.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 PathStateReceiver from 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:

Summary

  • conn.path_events() yields a live PathEventStream of added, removed, and selected path events for the full connection lifetime.
  • conn.paths() provides a cheap, read-only PathList snapshot with per-route RTT, address, and selection status.
  • The Connection struct holds path state in a PathStateReceiver field inside iroh/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →