Skip to main content

iroh_services/
net_diagnostics.rs

1//! Network diagnostics for iroh-powered applications.
2//!
3//! Collects a full network diagnostics report from an existing iroh Endpoint
4//! covering UDP connectivity, relay latency, and port mapping protocol
5//! availability.
6//!
7//! Relay latencies and UDP connectivity are read from iroh's [`NetReport`]
8//! which the endpoint already produces continuously. The only additional probe
9//! performed here is the port-mapping protocol availability check.
10use std::net::SocketAddr;
11
12use iroh::unstable_net_report::NetReport;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct DiagnosticsReport {
17    pub endpoint_id: iroh::EndpointId,
18    pub net_report: Option<NetReport>,
19    pub direct_addrs: Vec<SocketAddr>,
20    pub portmap_probe: Option<PortMapProbe>,
21    #[serde(default)]
22    pub iroh_version: String,
23    #[serde(default)]
24    pub iroh_services_version: String,
25}
26
27/// Port mapping protocol availability on the LAN.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct PortMapProbe {
30    pub upnp: bool,
31    pub pcp: bool,
32    pub nat_pmp: bool,
33}
34
35pub mod checks {
36    use std::{net::SocketAddr, time::Duration};
37
38    use anyhow::Result;
39    use iroh::{Endpoint, Watcher};
40
41    use super::*;
42
43    /// Run full network diagnostics on an existing endpoint. 10s timeout.
44    pub async fn run_diagnostics(endpoint: &Endpoint) -> Result<DiagnosticsReport> {
45        run_diagnostics_with_timeout(endpoint, Duration::from_secs(10)).await
46    }
47
48    /// Run full network diagnostics with a custom timeout for net report init.
49    async fn run_diagnostics_with_timeout(
50        endpoint: &Endpoint,
51        timeout: Duration,
52    ) -> Result<DiagnosticsReport> {
53        let endpoint_id = endpoint.id();
54
55        // 1. Wait for relay connection
56        if n0_future::time::timeout(timeout, endpoint.online())
57            .await
58            .is_err()
59        {
60            tracing::warn!("waiting for relay connection timed out after {timeout:?}");
61        }
62
63        // 2. Net report (includes relay latencies and UDP connectivity)
64        let mut watcher = endpoint.net_report();
65        let net_report = match n0_future::time::timeout(timeout, watcher.initialized()).await {
66            Ok(report) => Some(report),
67            Err(_) => {
68                tracing::warn!("net report timed out after {timeout:?}, using partial data");
69                watcher.get()
70            }
71        };
72
73        // 3. Endpoint address info
74        let addr = endpoint.addr();
75        let direct_addrs: Vec<SocketAddr> = addr.ip_addrs().copied().collect();
76
77        // 4. Port mapping probe (the one thing NetReport doesn't include)
78        #[cfg(not(target_arch = "wasm32"))]
79        let portmap_probe =
80            match n0_future::time::timeout(Duration::from_secs(5), probe_port_mapping()).await {
81                Ok(Ok(p)) => Some(p),
82                Ok(Err(e)) => {
83                    tracing::warn!("portmap probe failed: {e}");
84                    None
85                }
86                Err(_) => {
87                    tracing::warn!("portmap probe timed out");
88                    None
89                }
90            };
91
92        // TODO: setting `portmap_probe` to `None` makes svc fail to parse the report.
93        // Should be fixed there, but this works too for now.
94        #[cfg(target_arch = "wasm32")]
95        let portmap_probe = Some(PortMapProbe {
96            upnp: false,
97            pcp: false,
98            nat_pmp: false,
99        });
100
101        Ok(DiagnosticsReport {
102            endpoint_id,
103            net_report,
104            direct_addrs,
105            portmap_probe,
106            iroh_version: crate::IROH_VERSION.to_string(),
107            iroh_services_version: crate::IROH_SERVICES_VERSION.to_string(),
108        })
109    }
110
111    #[cfg(not(target_arch = "wasm32"))]
112    async fn probe_port_mapping() -> Result<PortMapProbe> {
113        let config = portmapper::Config {
114            enable_upnp: true,
115            enable_pcp: true,
116            enable_nat_pmp: true,
117            protocol: portmapper::Protocol::Udp,
118        };
119        let client = portmapper::Client::new(config);
120        let probe_rx = client.probe();
121        let probe = probe_rx.await?.map_err(|e| anyhow::anyhow!(e))?;
122        Ok(PortMapProbe {
123            upnp: probe.upnp,
124            pcp: probe.pcp,
125            nat_pmp: probe.nat_pmp,
126        })
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use iroh::endpoint::presets;
133
134    use crate::run_diagnostics;
135
136    #[tokio::test]
137    async fn test_run_diagnostics() {
138        let endpoint = iroh::Endpoint::builder(presets::Minimal)
139            .bind()
140            .await
141            .unwrap();
142        run_diagnostics(&endpoint).await.unwrap();
143        endpoint.close().await;
144    }
145}