How iroh Measures and Reports Network Performance Using NetReport
Iroh continuously evaluates network conditions through a three-stage pipeline that probes QUIC and HTTPS endpoints, aggregates the results into a structured Report, and publishes updates via an async Watcher API.
The NetReport subsystem in the n0-computer/iroh repository provides real-time visibility into NAT behavior, public IP addresses, and relay latency. This Rust-native implementation enables peer-to-peer applications to adapt dynamically to changing network topologies without blocking the main event loop.
The Three-Stage NetReport Pipeline
Iroh's network measurement process consists of tightly coupled stages for probing, aggregation, and publication. Each stage operates asynchronously and can be traced through specific source files in the iroh/src/net_report/ directory.
Stage 1: Probe Planning and Execution
When an Endpoint initializes, it spawns a net_report::Client that builds a probe plan based on current interface state (IfStateDetails) and configuration (NetReportConfig). The client executes two distinct probe types in parallel:
- QAD (QUIC Address Discovery) probes – Open QUIC connections to relay servers for IPv4 and IPv6, capturing the observed external address and measuring round-trip time (RTT).
- HTTPS latency probes – Execute empty GET requests against each relay’s
/generate_204endpoint to obtain lightweight latency measurements.
The probe execution is driven by net_report::reportgen::Client and helper functions run_probe_v4 and run_probe_v6 defined in iroh/src/net_report.rs (lines 57-66 and 92-124).
Stage 2: Report Aggregation
Each probe yields a ProbeReport that feeds into the mutable Report struct via the Report::update method in iroh/src/net_report/report.rs (lines 67-130). This aggregation logic:
- Records per-relay latency in
relay_latency - Sets
udp_v4andudp_v6flags upon successful QAD completion - Captures public addresses as
global_v4andglobal_v6 - Detects address-mapping variance to identify NAT behavior
- Flags
captive_portalwhen/generate_204returns a non-204 response
Stage 3: Real-Time Publication via Watchers
The endpoint stores the final Report in a Watchable<(Option<Report>, UpdateReason)> field inside the socket state. After Client::get_report completes, the associated Watcher notifies all consumers. Applications obtain a watcher through Endpoint::net_report() (defined in iroh/src/endpoint.rs lines 1330-1335), enabling reactive network monitoring without polling.
Key Source Files and Architecture
Understanding the following files is essential for customizing or debugging network measurements:
iroh/src/net_report.rs– CoreClientimplementation handling probe orchestration, history tracking viaadd_report_history_and_set_preferred_relay, and theFULL_REPORT_INTERVAL(5-minute) scheduling logic.iroh/src/net_report/report.rs– Definition of theReportstruct with fields likepreferred_relay,global_v4, and methods likehas_udp()for checking UDP reachability.iroh/src/net_report/reportgen.rs– Actor-based probe generation that manages concurrent HTTPS requests and QUIC handshakes.iroh/src/endpoint.rs– Public API entry point exposingnet_report()to return aWatcher.iroh/src/socket.rs– Maintains thenet_reportfield as aWatchablestate container.iroh/src/metrics.rs– TracksNetReportMetricsdistinguishing full versus incremental reports.
Implementing NetReport in Your Application
The following examples demonstrate how to consume network reports in Rust applications using iroh's async API.
Retrieving the Initial Report
Use initialized() to await the first completed network report before proceeding with peer connections:
use iroh::{Endpoint, endpoint::presets};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let ep = Endpoint::bind(presets::N0).await?;
// Blocks until the first report is ready
let report = ep.net_report().initialized().await;
if let Some(r) = report {
println!("Public IPv4: {:?}", r.global_v4);
println!("Public IPv6: {:?}", r.global_v6);
println!("Preferred relay: {:?}", r.preferred_relay);
println!("UDP reachable: {}", r.has_udp());
}
Ok(())
}
Subscribing to Continuous Updates
For long-running applications, treat the report as a stream to react to network changes in real time:
use iroh::{Endpoint, endpoint::presets};
use futures_util::StreamExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let ep = Endpoint::bind(presets::N0).await?;
let mut watcher = ep.net_report();
while let Some(opt) = watcher.next().await {
if let Some(r) = opt {
println!("Network changed - new preferred relay: {:?}", r.preferred_relay);
}
}
Ok(())
}
The Watcher remains valid for the Endpoint lifetime, making it suitable for background health monitoring tasks.
Analyzing Per-Relay Latency
Inspect individual probe results to build custom relay selection logic:
if let Some(report) = ep.net_report().initialized().await {
for (probe, relay, latency) in report.relay_latency.iter() {
println!("{:?} probe to {} took {:?}", probe, relay, latency);
}
}
The RelayLatencies::iter method (defined in iroh/src/net_report/report.rs lines 80-86) exposes raw timing data for both QAD and HTTPS probes.
Summary
- NetReport operates through three stages: probe execution (QAD/HTTPS), result aggregation via
Report::update, and publication throughWatcherstreams. - Source locations – Core logic resides in
iroh/src/net_report.rs,report.rs, andreportgen.rs, with public API exposure iniroh/src/endpoint.rs. - Probe types – QUIC address discovery probes determine public IPs and UDP viability, while HTTPS probes measure baseline relay latency.
- Access patterns – Use
initialized()for blocking initialization orWatcher::next()for continuous monitoring of network topology changes. - Report contents – Each
Reportcontainsglobal_v4/global_v6addresses,preferred_relayselection,relay_latencymappings, and NAT detection flags.
Frequently Asked Questions
What is the difference between QAD and HTTPS probes in iroh?
QAD (QUIC Address Discovery) probes establish actual QUIC connections to measure RTT and discover the host's public IP address through STUN-like mechanisms, while HTTPS probes perform lightweight HTTP GET requests to /generate_204 endpoints solely to measure latency without establishing persistent connections. QAD probes validate UDP reachability and NAT behavior, whereas HTTPS probes provide a cheap fallback for latency comparison when QUIC is blocked.
How often does iroh generate NetReport updates?
By default, iroh schedules full reports every 5 minutes (FULL_REPORT_INTERVAL). However, the system triggers major reports immediately upon detecting interface state changes through network_change() events. Incremental updates may occur more frequently when the Client determines that existing probe data is insufficient via the have_enough_reports check.
Can I access network reports without blocking my application?
Yes. While initialized() blocks until the first report completes, you can obtain a non-blocking Watcher immediately via Endpoint::net_report() and poll it asynchronously using .next().await in a background task. The watcher yields Option<Report> where None indicates that a report is still in progress, allowing your application to continue operating with degraded network awareness rather than waiting.
How does iroh determine the preferred relay server?
After each report completes, iroh calls add_report_history_and_set_preferred_relay to merge new latency data with historical measurements. The relay with the lowest recent latency across both QAD and HTTPS probes becomes the preferred_relay stored in the Report. This selection updates dynamically as network conditions change, ensuring optimal connection paths for peer-to-peer communication.
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 →