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
27impl DiagnosticsReport {
28    pub(crate) fn into_proto(self) -> iroh_services_proto::net_diagnostics::DiagnosticsReport {
29        iroh_services_proto::net_diagnostics::DiagnosticsReport {
30            endpoint_id: self.endpoint_id,
31            net_report: self.net_report,
32            direct_addrs: self.direct_addrs,
33            portmap_probe: self.portmap_probe.map(PortMapProbe::into_proto),
34            iroh_version: self.iroh_version,
35            iroh_services_version: self.iroh_services_version,
36        }
37    }
38}
39
40/// Port mapping protocol availability on the LAN.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct PortMapProbe {
43    pub upnp: bool,
44    pub pcp: bool,
45    pub nat_pmp: bool,
46}
47
48impl PortMapProbe {
49    fn into_proto(self) -> iroh_services_proto::net_diagnostics::PortMapProbe {
50        iroh_services_proto::net_diagnostics::PortMapProbe {
51            upnp: self.upnp,
52            pcp: self.pcp,
53            nat_pmp: self.nat_pmp,
54        }
55    }
56}
57
58pub mod checks {
59    use std::net::SocketAddr;
60
61    use anyhow::Result;
62    use iroh::{Endpoint, Watcher};
63    use n0_future::time::Duration;
64
65    use super::*;
66
67    /// Run full network diagnostics on an existing endpoint. 10s timeout.
68    pub async fn run_diagnostics(endpoint: &Endpoint) -> Result<DiagnosticsReport> {
69        run_diagnostics_with_timeout(endpoint, Duration::from_secs(10)).await
70    }
71
72    /// Run full network diagnostics with a custom timeout for net report init.
73    async fn run_diagnostics_with_timeout(
74        endpoint: &Endpoint,
75        timeout: Duration,
76    ) -> Result<DiagnosticsReport> {
77        let endpoint_id = endpoint.id();
78
79        // 1. Wait for relay connection
80        if n0_future::time::timeout(timeout, endpoint.online())
81            .await
82            .is_err()
83        {
84            tracing::warn!("waiting for relay connection timed out after {timeout:?}");
85        }
86
87        // 2. Net report (includes relay latencies and UDP connectivity)
88        let mut watcher = endpoint.net_report();
89        let net_report = match n0_future::time::timeout(timeout, watcher.initialized()).await {
90            Ok(report) => Some(report),
91            Err(_) => {
92                tracing::warn!("net report timed out after {timeout:?}, using partial data");
93                watcher.get()
94            }
95        };
96
97        // 3. Endpoint address info
98        let addr = endpoint.addr();
99        let direct_addrs: Vec<SocketAddr> = addr.ip_addrs().copied().collect();
100
101        // 4. Port mapping probe (the one thing NetReport doesn't include)
102        #[cfg(not(wasm_browser))]
103        let portmap_probe =
104            match n0_future::time::timeout(Duration::from_secs(5), probe_port_mapping()).await {
105                Ok(Ok(p)) => Some(p),
106                Ok(Err(e)) => {
107                    tracing::warn!("portmap probe failed: {e}");
108                    None
109                }
110                Err(_) => {
111                    tracing::warn!("portmap probe timed out");
112                    None
113                }
114            };
115
116        // TODO: setting `portmap_probe` to `None` makes svc fail to parse the report.
117        // Should be fixed there, but this works too for now.
118        #[cfg(wasm_browser)]
119        let portmap_probe = Some(PortMapProbe {
120            upnp: false,
121            pcp: false,
122            nat_pmp: false,
123        });
124
125        Ok(DiagnosticsReport {
126            endpoint_id,
127            net_report,
128            direct_addrs,
129            portmap_probe,
130            iroh_version: crate::IROH_VERSION.to_string(),
131            iroh_services_version: crate::IROH_SERVICES_VERSION.to_string(),
132        })
133    }
134
135    #[cfg(not(wasm_browser))]
136    async fn probe_port_mapping() -> Result<PortMapProbe> {
137        let config = portmapper::Config {
138            enable_upnp: true,
139            enable_pcp: true,
140            enable_nat_pmp: true,
141            protocol: portmapper::Protocol::Udp,
142        };
143        let client = portmapper::Client::new(config);
144        let probe_rx = client.probe();
145        let probe = probe_rx.await?.map_err(|e| anyhow::anyhow!(e))?;
146        Ok(PortMapProbe {
147            upnp: probe.upnp,
148            pcp: probe.pcp,
149            nat_pmp: probe.nat_pmp,
150        })
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use iroh::endpoint::presets;
157
158    use crate::run_diagnostics;
159
160    #[tokio::test]
161    async fn test_run_diagnostics() {
162        let endpoint = iroh::Endpoint::builder(presets::Minimal)
163            .bind()
164            .await
165            .unwrap();
166        run_diagnostics(&endpoint).await.unwrap();
167        endpoint.close().await;
168    }
169}