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_204 endpoint 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_v4 and udp_v6 flags upon successful QAD completion
  • Captures public addresses as global_v4 and global_v6
  • Detects address-mapping variance to identify NAT behavior
  • Flags captive_portal when /generate_204 returns 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 – Core Client implementation handling probe orchestration, history tracking via add_report_history_and_set_preferred_relay, and the FULL_REPORT_INTERVAL (5-minute) scheduling logic.
  • iroh/src/net_report/report.rs – Definition of the Report struct with fields like preferred_relay, global_v4, and methods like has_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 exposing net_report() to return a Watcher.
  • iroh/src/socket.rs – Maintains the net_report field as a Watchable state container.
  • iroh/src/metrics.rs – Tracks NetReportMetrics distinguishing 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 through Watcher streams.
  • Source locations – Core logic resides in iroh/src/net_report.rs, report.rs, and reportgen.rs, with public API exposure in iroh/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 or Watcher::next() for continuous monitoring of network topology changes.
  • Report contents – Each Report contains global_v4/global_v6 addresses, preferred_relay selection, relay_latency mappings, 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:

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 →