How Do Iroh Network Report Probes Work: QUIC, HTTPS, and Relay Selection

Iroh network report probes measure reachability and latency to relay servers by executing a planned sequence of HTTPS and QUIC Address Discovery (QAD) probes, then aggregating the results to select the lowest-latency relay while accounting for NAT behavior.

Iroh continuously evaluates network conditions to optimize peer-to-peer connectivity through relay servers. The iroh network report probes system generates periodic reports by orchestrating asynchronous HTTPS and QUIC probes, analyzing the results, and maintaining a history of relay performance. This mechanism, implemented in the n0-computer/iroh repository, enables intelligent relay selection based on real-time latency measurements and observed external addresses.

Probe Planning: Creating the ProbePlan

The probe process begins in iroh/src/net_report/probes.rs where the ProbePlan::initial function constructs a deterministic schedule of measurements for every relay in the RelayMap.

The Probe enum defines the three supported probe types:

pub enum Probe {
    Https,
    #[cfg(not(wasm_browser))] QadIpv4,
    #[cfg(not(wasm_browser))] QadIpv6,
}

For each relay, the planner creates a ProbeSet containing multiple attempts. HTTPS probes are scheduled with three retry delays calculated as HTTPS_OFFSET + DEFAULT_INITIAL_RETRANSMIT * attempt, producing intervals of 200 ms, 300 ms, and 400 ms. When QUIC probing is enabled via options.quic_config, additional QAD sets are appended. The plan stores these sets in a BTreeSet<ProbeSet> to ensure deterministic ordering and automatic deduplication.

Probe Execution: Running HTTPS and QUIC Probes

Execution occurs in iroh/src/net_report.rs where the spawn_qad_probes function manages concurrent probe tasks using a Tokio JoinSet.

QUIC Address Discovery (QAD) probes execute via run_probe_v4 and run_probe_v6. These functions resolve the relay’s IP address using reportgen::get_relay_addr_ipv4/ipv6, establish a QUIC connection via quic_client.create_conn, and monitor the observed external address:

let conn = quic_client.create_conn(relay_addr.into(), host).await?;
let mut watcher = conn.observed_external_addr();
let addr = watcher.next().await.ok_or(QadProbeError::ReceiverDropped)?;

The initial QadProbeReport returns immediately, while a background task pushes subsequent address updates into a Watchable<Option<QadProbeReport>> to detect NAT changes during the reporting period.

HTTPS probes perform an empty GET request to /generate_204 on each relay, measuring the round-trip latency.

The system limits concurrency to MAX_RELAYS (5) per IP version, wrapping each probe in a PROBES_TIMEOUT duration and a cancellation token for clean shutdown:

v4_buf.spawn(
    cancel_v4.child_token()
        .run_until_cancelled_owned(time::timeout(PROBES_TIMEOUT,
            run_probe_v4(relay, quic_client, dns_resolver, inner_token)))
        .instrument(info_span!("QADv4", %relay_url)),
);

Report Aggregation and Relay Selection

The get_report function in iroh/src/net_report.rs receives probe results via a channel (probe_rx) and monitors QAD streams (qad_conns.watch_v4/v6). After each result, have_enough_reports checks whether the report is complete—requiring at least one IPv4 and one IPv6 QAD result when both stacks are present, or HTTPS results per relay.

Once sufficient data is collected, add_report_history_and_set_preferred_relay merges current latency measurements with a rolling five-minute history. It selects the preferred_relay with the lowest latency while applying a hysteresis rule to avoid unnecessary switching:

if prev_relay.is_some()
    && r.preferred_relay != prev_relay
    && !old_relay_cur_latency.is_zero()
    && best_any > old_relay_cur_latency / 3 * 2
{
    r.preferred_relay = prev_relay;
}

This logic retains the current relay unless the alternative is significantly faster (less than two-thirds of the current latency).

Code Example: Generating a Network Report

The following Rust example demonstrates how to initialize the reporting client and request a network report:

use iroh::net_report::{Client, Options, Report};
use iroh_relay::RelayMap;
use iroh_dns::dns::DnsResolver;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;

#[tokio::main]
async fn main() {
    // 1️⃣ Build a RelayMap (e.g. from a list of known relays)
    let relay_map = RelayMap::empty(); // replace with real relays

    // 2️⃣ Create DNS resolver and TLS config (required for QUIC)
    let dns = DnsResolver::new();
    let opts = Options::default(); // enables HTTPS + QUIC probes

    // 3️⃣ Initialise the client
    let mut client = Client::new(dns, relay_map, opts, Default::default());

    // 4️⃣ Request a network report
    let if_state = client::net_report::IfStateDetails::default(); // detect IPv4/IPv6 capability
    let cancel = CancellationToken::new();
    let report: Report = client
        .get_report(if_state, /*is_major=*/ true, cancel.child_token())
        .await;

    println!("Preferred relay: {:?}", report.preferred_relay);
    println!("Relay latencies: {:?}", report.relay_latency);
}

Running this code initiates the probe plan, fires HTTPS and QUIC probes concurrently, and outputs the selected relay and measured latencies.

Key Source Files and Structures

Summary

  • ProbePlanning creates a deterministic schedule of HTTPS and QAD probes for each relay with calculated retry delays.
  • ProbeExecution spawns up to 5 concurrent QUIC probes per IP version and HTTPS probes, using Tokio JoinSet with cancellation tokens and timeouts.
  • QAD probes discover external addresses by opening QUIC connections and watching observed_external_addr() for NAT detection.
  • ReportAggregation determines completion via have_enough_reports, then selects the preferred relay using latency history and hysteresis to prevent flapping.
  • The system runs full reports every 5 minutes and on-demand for first connections, providing continuous network condition monitoring.

Frequently Asked Questions

What is the difference between HTTPS and QUIC probes in iroh?

HTTPS probes measure basic reachability and latency by issuing an empty GET request to /generate_204 on the relay server. QUIC Address Discovery (QAD) probes open an actual QUIC connection to discover the node's external IP address and port, enabling NAT detection and endpoint mapping. While HTTPS probes provide simple latency metrics, QAD probes reveal how the network translates internal addresses.

How does iroh decide when to stop probing?

The have_enough_reports function evaluates whether the current report contains sufficient data for decision-making. According to the implementation in iroh/src/net_report.rs, the report is considered complete when it contains at least one IPv4 and one IPv6 QAD result (if both stacks are available) or adequate HTTPS probe results per relay. Once satisfied, the probe actor drops, cancelling remaining probes via the shutdown token to conserve resources.

How does iroh select the preferred relay?

The add_report_history_and_set_preferred_relay function maintains a rolling five-minute history of latency measurements. It selects the relay with the lowest recent latency as the new preferred relay. However, it applies a hysteresis rule: if the current relay is performing adequately, the system will not switch to a new relay unless the alternative's latency is less than two-thirds of the current latency (best_any > old_relay_cur_latency / 3 * 2). This prevents rapid switching between similarly-performing relays.

What happens when NAT changes during probing?

Because QAD probes return the initial QadProbeReport immediately but continue monitoring via a Watchable<Option<QadProbeReport>> in the background, the report generator can detect address changes during the probe period. If the external address observed over the QUIC connection changes, the background task updates the watchable, and get_report incorporates these updates into the final report, ensuring the network state reflects current NAT mappings rather than just initial discoveries.

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 →