From a10793f134d5faa3511719f3d852c319347ffdca Mon Sep 17 00:00:00 2001 From: vmenge Date: Wed, 18 Feb 2026 20:32:45 +0100 Subject: [PATCH 1/5] chore(connd): print a conn report everytime connection state changes --- Cargo.lock | 2 + orb-connd/Cargo.toml | 3 + orb-connd/src/connectivity_daemon.rs | 3 + orb-connd/src/lib.rs | 1 + orb-connd/src/main.rs | 6 +- orb-connd/src/network_manager/mod.rs | 66 ++++++- orb-connd/src/reporters/mod.rs | 5 +- .../src/reporters/net_changed_reporter.rs | 153 ++++++++++++++- orb-connd/src/resolved.rs | 185 ++++++++++++++++++ orb-connd/tests/fixture.rs | 2 + 10 files changed, 414 insertions(+), 12 deletions(-) create mode 100644 orb-connd/src/resolved.rs diff --git a/Cargo.lock b/Cargo.lock index bc2916d0d..122db3f91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8200,6 +8200,7 @@ dependencies = [ "proptest", "rand 0.8.5", "regex", + "reqwest 0.12.24", "rkyv", "rustix 0.38.44", "rusty_network_manager", @@ -8218,6 +8219,7 @@ dependencies = [ "uuid 1.19.0", "uzers", "zbus", + "zbus_systemd", "zenorb", ] diff --git a/orb-connd/Cargo.toml b/orb-connd/Cargo.toml index eae927132..07762ea8f 100644 --- a/orb-connd/Cargo.toml +++ b/orb-connd/Cargo.toml @@ -43,6 +43,8 @@ p256.workspace = true prelude.workspace = true rand.workspace = true regex.workspace = true +reqwest.workspace = true +nix = { workspace = true, features = ["net"] } rustix = { workspace = true, features = ["process"] } rusty_network_manager = "0.6.0" rkyv = { workspace = true, features = ["validation"] } @@ -58,6 +60,7 @@ tracing.workspace = true uuid = { workspace = true, features = ["v4"] } uzers = "0.12.0" zbus.workspace = true +zbus_systemd = { workspace = true, features = ["resolve1"] } zenorb.workspace = true [dev-dependencies] diff --git a/orb-connd/src/connectivity_daemon.rs b/orb-connd/src/connectivity_daemon.rs index 9088d3f46..42e158d02 100644 --- a/orb-connd/src/connectivity_daemon.rs +++ b/orb-connd/src/connectivity_daemon.rs @@ -1,5 +1,6 @@ use crate::modem_manager::ModemManager; use crate::network_manager::NetworkManager; +use crate::resolved::Resolved; use crate::service::{ConndService, ProfileStorage}; use crate::statsd::StatsdClient; use crate::{reporters, OrbCapabilities, Tasks}; @@ -17,6 +18,7 @@ pub async fn program( sysfs: impl AsRef, usr_persistent: impl AsRef, network_manager: NetworkManager, + resolved: Resolved, session_bus: zbus::Connection, os_release: OrbOsRelease, statsd_client: impl StatsdClient, @@ -57,6 +59,7 @@ pub async fn program( tasks.extend( reporters::spawn( network_manager, + resolved, session_bus, modem_manager, statsd_client, diff --git a/orb-connd/src/lib.rs b/orb-connd/src/lib.rs index 2067c835e..f476e2c91 100644 --- a/orb-connd/src/lib.rs +++ b/orb-connd/src/lib.rs @@ -7,6 +7,7 @@ pub mod connectivity_daemon; pub mod modem_manager; pub mod network_manager; pub mod reporters; +pub mod resolved; pub mod secure_storage; pub mod service; pub mod statsd; diff --git a/orb-connd/src/main.rs b/orb-connd/src/main.rs index d74445476..d65673b7a 100644 --- a/orb-connd/src/main.rs +++ b/orb-connd/src/main.rs @@ -5,6 +5,7 @@ use orb_connd::{ connectivity_daemon, modem_manager::cli::ModemManagerCli, network_manager::NetworkManager, + resolved::Resolved, secure_storage::{self, ConndStorageScopes, SecureStorage}, service::ProfileStorage, statsd::dd::DogstatsdClient, @@ -74,10 +75,12 @@ fn connectivity_daemon() -> Result<()> { rt.block_on(async { let os_release = OrbOsRelease::read().await?; + let system_bus = zbus::Connection::system().await?; let nm = NetworkManager::new( - zbus::Connection::system().await?, + system_bus.clone(), WpaCli::new(os_release.orb_os_platform_type), ); + let resolved = Resolved::new(system_bus); let cancel_token = CancellationToken::new(); let profile_storage = match os_release.orb_os_platform_type { @@ -103,6 +106,7 @@ fn connectivity_daemon() -> Result<()> { .sysfs("/sys") .usr_persistent("/usr/persistent") .network_manager(nm) + .resolved(resolved) .session_bus(zbus::Connection::session().await?) .os_release(os_release) .statsd_client(DogstatsdClient::new()) diff --git a/orb-connd/src/network_manager/mod.rs b/orb-connd/src/network_manager/mod.rs index 50133fd25..5d63c454b 100644 --- a/orb-connd/src/network_manager/mod.rs +++ b/orb-connd/src/network_manager/mod.rs @@ -12,8 +12,9 @@ use rusty_network_manager::{ NM80211Mode, NMActiveConnectionState, NMConnectivityState, NMDeviceType, NMState, }, - AccessPointProxy, ActiveProxy, DeviceProxy, NM80211ApFlags, NM80211ApSecurityFlags, - NetworkManagerProxy, SettingsConnectionProxy, SettingsProxy, WirelessProxy, + AccessPointProxy, ActiveProxy, DeviceProxy, IP4ConfigProxy, IP6ConfigProxy, + NM80211ApFlags, NM80211ApSecurityFlags, NetworkManagerProxy, + SettingsConnectionProxy, SettingsProxy, WirelessProxy, }; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc, time::Duration}; @@ -454,15 +455,70 @@ impl NetworkManager { let mut ifaces = Vec::with_capacity(dev_paths.len()); for dp in dev_paths { let dev = DeviceProxy::new_from_path(dp, &self.conn).await?; - ifaces.push(dev.interface().await?); + let iface = match dev.ip_interface().await { + Ok(ip) if !ip.is_empty() => ip, + _ => dev.interface().await?, + }; + ifaces.push(iface); } + let (ipv4_gateway, ipv4_dns) = match ac.ip4_config().await { + Ok(ip4_path) if ip4_path.as_str() != "/" => { + let ip4 = + IP4ConfigProxy::new_from_path(ip4_path, &self.conn).await?; + + let gateway = ip4.gateway().await.ok().filter(|g| !g.is_empty()); + + let dns = ip4 + .nameserver_data() + .await + .unwrap_or_default() + .into_iter() + .filter_map(|entry| { + let val = entry.get("address")?; + let s: &str = val.downcast_ref().ok()?; + Some(s.to_string()) + }) + .collect(); + + (gateway, dns) + } + _ => (None, Vec::new()), + }; + + let (ipv6_gateway, ipv6_dns) = match ac.ip6_config().await { + Ok(ip6_path) if ip6_path.as_str() != "/" => { + let ip6 = + IP6ConfigProxy::new_from_path(ip6_path, &self.conn).await?; + + let gateway = ip6.gateway().await.ok().filter(|g| !g.is_empty()); + + let dns = ip6 + .nameservers() + .await + .unwrap_or_default() + .into_iter() + .filter_map(|bytes| { + let octets: [u8; 16] = bytes.try_into().ok()?; + Some(std::net::Ipv6Addr::from(octets).to_string()) + }) + .collect(); + + (gateway, dns) + } + _ => (None, Vec::new()), + }; + out.push(ActiveConn { id, uuid, state, devices: ifaces, conn_path, + ipv4_gateway, + ipv4_dns, + ipv6_gateway, + ipv6_dns, }); } @@ -960,6 +1016,10 @@ pub struct ActiveConn { pub state: ActiveConnState, pub devices: Vec, pub conn_path: OwnedObjectPath, + pub ipv4_gateway: Option, + pub ipv4_dns: Vec, + pub ipv6_gateway: Option, + pub ipv6_dns: Vec, } #[cfg(test)] diff --git a/orb-connd/src/reporters/mod.rs b/orb-connd/src/reporters/mod.rs index 2224d4819..d498ee674 100644 --- a/orb-connd/src/reporters/mod.rs +++ b/orb-connd/src/reporters/mod.rs @@ -2,6 +2,7 @@ use crate::{ modem_manager::ModemManager, network_manager::NetworkManager, reporters::modem_status::ModemStatus, + resolved::Resolved, statsd::StatsdClient, utils::{retry_for, State}, OrbCapabilities, Tasks, @@ -23,8 +24,10 @@ pub mod modem_status; pub mod net_changed_reporter; pub mod net_stats; +#[allow(clippy::too_many_arguments)] pub async fn spawn( nm: NetworkManager, + resolved: Resolved, session_bus: zbus::Connection, modem_manager: Arc, statsd_client: impl StatsdClient, @@ -43,7 +46,7 @@ pub async fn spawn( session_bus.clone(), Duration::from_secs(30), ), - net_changed_reporter::spawn(nm, zsender), + net_changed_reporter::spawn(nm, resolved, zsender), ]); if let OrbCapabilities::CellularAndWifi = cap { diff --git a/orb-connd/src/reporters/net_changed_reporter.rs b/orb-connd/src/reporters/net_changed_reporter.rs index ccc3b7191..f3f4c1df1 100644 --- a/orb-connd/src/reporters/net_changed_reporter.rs +++ b/orb-connd/src/reporters/net_changed_reporter.rs @@ -1,23 +1,29 @@ use crate::network_manager::{Connection, NetworkManager}; +use crate::resolved::Resolved; use color_eyre::{eyre::eyre, Result}; use futures::StreamExt; use orb_connd_events::ConnectionKind; use rusty_network_manager::dbus_interface_types::NMState; -use std::time::Duration; +use std::fmt::Write; +use std::time::{Duration, Instant}; use tokio::{ task::{self, JoinHandle}, time, }; -use tracing::{error, info}; +use tracing::{error, info, warn}; static BACKOFF: Duration = Duration::from_secs(5); -pub fn spawn(nm: NetworkManager, zsender: zenorb::Sender) -> JoinHandle> { +pub fn spawn( + nm: NetworkManager, + resolved: Resolved, + zsender: zenorb::Sender, +) -> JoinHandle> { info!("starting net_changed reporter"); task::spawn(async move { loop { - if let Err(e) = report_loop(&nm, &zsender).await { + if let Err(e) = report_loop(&nm, &resolved, &zsender).await { error!(error = ?e, "net changed loop error, retrying in {}s. error: {e}", BACKOFF.as_secs()); } @@ -26,13 +32,17 @@ pub fn spawn(nm: NetworkManager, zsender: zenorb::Sender) -> JoinHandle Result<()> { +async fn report_loop( + nm: &NetworkManager, + resolved: &Resolved, + zsender: &zenorb::Sender, +) -> Result<()> { let publisher = zsender.publisher("net/changed")?; let mut state_stream = nm.state_stream().await?; let mut primary_conn_stream = nm.primary_connection_stream().await?; - let mut conn_event = - connection_event(nm.state().await?, nm.primary_connection().await?); + let nm_state = nm.state().await?; + let mut conn_event = connection_event(nm_state, nm.primary_connection().await?); let bytes = rkyv::to_bytes::<_, 64>(&conn_event)?; publisher @@ -40,6 +50,8 @@ async fn report_loop(nm: &NetworkManager, zsender: &zenorb::Sender) -> Result<() .await .map_err(|e| eyre!("{e}"))?; + report(nm, resolved, &conn_event).await?; + loop { tokio::select! { _ = state_stream.next() => {} @@ -58,6 +70,8 @@ async fn report_loop(nm: &NetworkManager, zsender: &zenorb::Sender) -> Result<() .put(bytes.into_vec()) .await .map_err(|e| eyre!("{e}"))?; + + report(nm, resolved, &conn_event).await?; } } } @@ -83,3 +97,128 @@ fn connection_event( _ => Disconnected, } } + +async fn report( + nm: &NetworkManager, + resolved: &Resolved, + conn_event: &orb_connd_events::Connection, +) -> Result<()> { + let active_conns = nm.active_connections().await?; + let connectivity_uri = nm.connectivity_check_uri().await?; + let hostname = hostname_from_uri(&connectivity_uri); + + let mut msg = String::new(); + writeln!(msg, "network report:")?; + writeln!(msg, " primary connection: {conn_event:?}")?; + + for conn in &active_conns { + writeln!(msg, " [{}]: {conn:?}", conn.id)?; + + for iface in &conn.devices { + match resolved.link_status(iface).await { + Ok(status) => { + writeln!(msg, " [{iface}] resolvectl status: {status:?}")? + } + + Err(e) => { + warn!(iface, error = ?e, "[{iface}] resolvectl status failed") + } + } + + let Some(hostname) = hostname else { continue }; + match resolved.resolve_hostname(iface, hostname).await { + Ok(resolution) => writeln!( + msg, + " [{iface}] resolvectl query {hostname}: {resolution:?}" + )?, + + Err(e) => { + warn!(iface, host = hostname, error = ?e, "[{iface}] resolvectl query {hostname} failed") + } + } + } + } + + match connectivity_check(&connectivity_uri).await { + Ok(check) => { + writeln!(msg, " connectivity check GET {connectivity_uri}:")?; + writeln!(msg, " status: {}", check.status)?; + if let Some(loc) = &check.location { + writeln!(msg, " Location: {loc}")?; + } + if let Some(nms) = &check.nm_status { + writeln!(msg, " X-NetworkManager-Status: {nms}")?; + } + if let Some(cl) = &check.content_length { + writeln!(msg, " Content-Length: {cl}")?; + } + writeln!(msg, " elapsed: {}ms", check.elapsed.as_millis())?; + } + + Err(e) => { + warn!( + uri = connectivity_uri, + error = ?e, + "connectivity check GET {connectivity_uri} failed" + ); + } + } + + info!("{msg}"); + + Ok(()) +} + +#[derive(Debug)] +struct ConnectivityCheck { + status: reqwest::StatusCode, + location: Option, + nm_status: Option, + content_length: Option, + elapsed: Duration, +} + +async fn connectivity_check(uri: &str) -> Result { + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build()?; + let start = Instant::now(); + let resp = client.get(uri).send().await?; + let elapsed = start.elapsed(); + + let status = resp.status(); + let location = resp + .headers() + .get("location") + .and_then(|v| v.to_str().ok()) + .map(str::to_string); + let nm_status = resp + .headers() + .get("x-networkmanager-status") + .and_then(|v| v.to_str().ok()) + .map(str::to_string); + let content_length = resp + .headers() + .get("content-length") + .and_then(|v| v.to_str().ok()) + .map(str::to_string); + + Ok(ConnectivityCheck { + status, + location, + nm_status, + content_length, + elapsed, + }) +} + +fn hostname_from_uri(uri: &str) -> Option<&str> { + let after_scheme = uri.split("://").nth(1)?; + let host_and_rest = after_scheme.split('/').next()?; + let host = host_and_rest.split(':').next()?; + if host.is_empty() { + None + } else { + Some(host) + } +} diff --git a/orb-connd/src/resolved.rs b/orb-connd/src/resolved.rs new file mode 100644 index 000000000..307e6f22e --- /dev/null +++ b/orb-connd/src/resolved.rs @@ -0,0 +1,185 @@ +use color_eyre::{eyre::Context, Result}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use tracing::warn; +use zbus_systemd::resolve1; + +/// Client for systemd-resolved, providing DNS resolution and resolver status +/// via the `org.freedesktop.resolve1` D-Bus interface. +#[derive(Clone)] +pub struct Resolved { + conn: zbus::Connection, +} + +impl Resolved { + pub fn new(conn: zbus::Connection) -> Self { + Self { conn } + } + + pub async fn resolve_hostname( + &self, + iface: &str, + hostname: &str, + ) -> Result { + let ifindex = nix::net::if_::if_nametoindex(iface) + .wrap_err_with(|| format!("unknown interface: {iface}"))? + as i32; + let proxy = resolve1::ManagerProxy::new(&self.conn).await?; + let (raw_addrs, canonical_name, flags) = proxy + .resolve_hostname(ifindex, hostname.to_string(), 0, 0) + .await?; + + let addresses = raw_addrs + .into_iter() + .filter_map(|(_ifindex, family, bytes)| parse_ip(family, &bytes)) + .collect(); + + Ok(HostnameResolution { + addresses, + canonical_name, + flags: ResolveFlags::from_raw(flags), + }) + } + + pub async fn link_status(&self, iface: &str) -> Result { + let ifindex = nix::net::if_::if_nametoindex(iface) + .wrap_err_with(|| format!("unknown interface: {iface}"))? + as i32; + let manager = resolve1::ManagerProxy::new(&self.conn).await?; + let link_path = manager.get_link(ifindex).await?; + let link = resolve1::LinkProxy::builder(&self.conn) + .path(link_path)? + .build() + .await?; + + let current_dns_server = link + .current_dns_server() + .await + .inspect_err(|e| warn!("failed to get current DNS server for {iface}: {e}")) + .ok() + .and_then(|(family, bytes)| parse_ip(family, &bytes)); + + let dns_servers = link + .dns() + .await? + .into_iter() + .filter_map(|(family, bytes)| parse_ip(family, &bytes)) + .collect(); + + let domains = link + .domains() + .await? + .into_iter() + .map(|(domain, is_routing_domain)| DnsDomain { + domain, + is_routing_domain, + }) + .collect(); + + let default_route = link + .default_route() + .await + .inspect_err(|e| warn!("failed to get default route for {iface}: {e}")) + .unwrap_or(false); + + Ok(LinkDnsStatus { + current_dns_server, + dns_servers, + domains, + default_route, + }) + } +} + +/// AF_INET from +const AF_INET: i32 = 2; +/// AF_INET6 from +const AF_INET6: i32 = 10; + +fn parse_ip(family: i32, bytes: &[u8]) -> Option { + match family { + AF_INET if bytes.len() == 4 => { + let octets: [u8; 4] = bytes.try_into().ok()?; + Some(IpAddr::V4(Ipv4Addr::from(octets))) + } + + AF_INET6 if bytes.len() == 16 => { + let octets: [u8; 16] = bytes.try_into().ok()?; + Some(IpAddr::V6(Ipv6Addr::from(octets))) + } + + _ => None, + } +} + +/// Result of resolving a hostname via systemd-resolved. +#[derive(Debug)] +pub struct HostnameResolution { + /// IP addresses the hostname resolved to. + pub addresses: Vec, + /// Canonical hostname returned by the resolver. + pub canonical_name: String, + /// Flags indicating origin and security properties of the response. + pub flags: ResolveFlags, +} + +/// Flags returned by systemd-resolved indicating how a query was answered. +/// +/// Bit positions sourced from: +/// +#[derive(Debug)] +pub struct ResolveFlags { + /// The answer came (at least partially) from the local cache. + pub from_cache: bool, + /// The answer came (at least partially) from the network. + pub from_network: bool, + /// The answer was (at least partially) synthesized locally. + pub synthetic: bool, + /// The answer came (at least partially) from a locally registered zone. + pub from_zone: bool, + /// The answer came (at least partially) from local trust anchors. + pub from_trust_anchor: bool, + /// The returned data has been fully authenticated (e.g. DNSSEC). + pub authenticated: bool, + /// The query was resolved via encrypted channels or never left this + /// system. + pub confidential: bool, +} + +impl ResolveFlags { + fn from_raw(flags: u64) -> Self { + Self { + from_cache: flags & (1 << 20) != 0, + from_network: flags & (1 << 23) != 0, + synthetic: flags & (1 << 19) != 0, + from_zone: flags & (1 << 21) != 0, + from_trust_anchor: flags & (1 << 22) != 0, + authenticated: flags & (1 << 9) != 0, + confidential: flags & (1 << 18) != 0, + } + } +} + +/// A search or routing domain configured in systemd-resolved. +#[derive(Debug)] +pub struct DnsDomain { + /// The domain name. + pub domain: String, + /// If true, this is a routing-only domain (prefixed with `~` in + /// `resolvectl status` output) used to route queries to specific DNS + /// servers without being used for search. + pub is_routing_domain: bool, +} + +/// Per-link DNS status from systemd-resolved, equivalent to the per-link +/// section of `resolvectl status`. +#[derive(Debug)] +pub struct LinkDnsStatus { + /// The DNS server currently being used for queries on this link. + pub current_dns_server: Option, + /// All configured DNS servers on this link. + pub dns_servers: Vec, + /// Search and routing domains configured on this link. + pub domains: Vec, + /// Whether this link is used as the default route for DNS queries. + pub default_route: bool, +} diff --git a/orb-connd/tests/fixture.rs b/orb-connd/tests/fixture.rs index 543dfa844..7a31867b0 100644 --- a/orb-connd/tests/fixture.rs +++ b/orb-connd/tests/fixture.rs @@ -12,6 +12,7 @@ use orb_connd::{ ModemManager, Signal, SimId, SimInfo, }, network_manager::NetworkManager, + resolved::Resolved, secure_storage::{ConndStorageScopes, SecureStorage}, service::ProfileStorage, statsd::StatsdClient, @@ -171,6 +172,7 @@ impl Fixture { }) .modem_manager(modem_manager.unwrap_or_else(default_mockmmcli)) .network_manager(nm.clone()) + .resolved(Resolved::new(conn.clone())) .statsd_client(statsd.unwrap_or(MockStatsd)) .sysfs(sysfs.clone()) .usr_persistent(usr_persistent.clone()) From 4da280e3719ecde90e270c91695bc993ee2f5c3d Mon Sep 17 00:00:00 2001 From: vmenge Date: Wed, 18 Feb 2026 20:37:06 +0100 Subject: [PATCH 2/5] chore(connd): conn report only if we have some sort of connection --- .../src/reporters/net_changed_reporter.rs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/orb-connd/src/reporters/net_changed_reporter.rs b/orb-connd/src/reporters/net_changed_reporter.rs index f3f4c1df1..b12d92fad 100644 --- a/orb-connd/src/reporters/net_changed_reporter.rs +++ b/orb-connd/src/reporters/net_changed_reporter.rs @@ -50,7 +50,9 @@ async fn report_loop( .await .map_err(|e| eyre!("{e}"))?; - report(nm, resolved, &conn_event).await?; + if is_connected(&conn_event) { + report(nm, resolved, &conn_event).await?; + } loop { tokio::select! { @@ -71,7 +73,9 @@ async fn report_loop( .await .map_err(|e| eyre!("{e}"))?; - report(nm, resolved, &conn_event).await?; + if is_connected(&conn_event) { + report(nm, resolved, &conn_event).await?; + } } } } @@ -98,6 +102,15 @@ fn connection_event( } } +fn is_connected(conn_event: &orb_connd_events::Connection) -> bool { + matches!( + conn_event, + orb_connd_events::Connection::ConnectedGlobal(_) + | orb_connd_events::Connection::ConnectedSite(_) + | orb_connd_events::Connection::ConnectedLocal(_) + ) +} + async fn report( nm: &NetworkManager, resolved: &Resolved, @@ -141,7 +154,7 @@ async fn report( match connectivity_check(&connectivity_uri).await { Ok(check) => { - writeln!(msg, " connectivity check GET {connectivity_uri}:")?; + writeln!(msg, " connectivity check GET ok {connectivity_uri}:")?; writeln!(msg, " status: {}", check.status)?; if let Some(loc) = &check.location { writeln!(msg, " Location: {loc}")?; @@ -159,7 +172,7 @@ async fn report( warn!( uri = connectivity_uri, error = ?e, - "connectivity check GET {connectivity_uri} failed" + "connectivity check GET failed {connectivity_uri}" ); } } @@ -181,6 +194,7 @@ struct ConnectivityCheck { async fn connectivity_check(uri: &str) -> Result { let client = reqwest::Client::builder() .redirect(reqwest::redirect::Policy::none()) + .timeout(Duration::from_secs(5)) .build()?; let start = Instant::now(); let resp = client.get(uri).send().await?; From 373de1f10602eab69f5c5b4b13ab40923a73328c Mon Sep 17 00:00:00 2001 From: vmenge Date: Wed, 18 Feb 2026 20:39:13 +0100 Subject: [PATCH 3/5] chore(connd): conn report more specific http failure --- orb-connd/src/reporters/net_changed_reporter.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/orb-connd/src/reporters/net_changed_reporter.rs b/orb-connd/src/reporters/net_changed_reporter.rs index b12d92fad..16c2c1e1e 100644 --- a/orb-connd/src/reporters/net_changed_reporter.rs +++ b/orb-connd/src/reporters/net_changed_reporter.rs @@ -154,7 +154,8 @@ async fn report( match connectivity_check(&connectivity_uri).await { Ok(check) => { - writeln!(msg, " connectivity check GET ok {connectivity_uri}:")?; + let result = if check.status.is_success() { "ok" } else { "fail" }; + writeln!(msg, " connectivity check GET {result} {connectivity_uri}:")?; writeln!(msg, " status: {}", check.status)?; if let Some(loc) = &check.location { writeln!(msg, " Location: {loc}")?; @@ -172,7 +173,7 @@ async fn report( warn!( uri = connectivity_uri, error = ?e, - "connectivity check GET failed {connectivity_uri}" + "connectivity check GET timeout {connectivity_uri}" ); } } From 0a0be1d9f560320abea66a050675e5125f81f350 Mon Sep 17 00:00:00 2001 From: vmenge Date: Wed, 18 Feb 2026 20:53:07 +0100 Subject: [PATCH 4/5] fmt --- orb-connd/src/reporters/net_changed_reporter.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/orb-connd/src/reporters/net_changed_reporter.rs b/orb-connd/src/reporters/net_changed_reporter.rs index 16c2c1e1e..cf8261946 100644 --- a/orb-connd/src/reporters/net_changed_reporter.rs +++ b/orb-connd/src/reporters/net_changed_reporter.rs @@ -154,7 +154,11 @@ async fn report( match connectivity_check(&connectivity_uri).await { Ok(check) => { - let result = if check.status.is_success() { "ok" } else { "fail" }; + let result = if check.status.is_success() { + "ok" + } else { + "fail" + }; writeln!(msg, " connectivity check GET {result} {connectivity_uri}:")?; writeln!(msg, " status: {}", check.status)?; if let Some(loc) = &check.location { From bb2dafc5c1f8b6e0f02431668ea12a5980ff7299 Mon Sep 17 00:00:00 2001 From: vmenge Date: Thu, 19 Feb 2026 17:29:49 +0100 Subject: [PATCH 5/5] share http client --- .../src/reporters/net_changed_reporter.rs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/orb-connd/src/reporters/net_changed_reporter.rs b/orb-connd/src/reporters/net_changed_reporter.rs index cf8261946..a742dafd2 100644 --- a/orb-connd/src/reporters/net_changed_reporter.rs +++ b/orb-connd/src/reporters/net_changed_reporter.rs @@ -1,5 +1,6 @@ use crate::network_manager::{Connection, NetworkManager}; use crate::resolved::Resolved; +use color_eyre::eyre::Context; use color_eyre::{eyre::eyre, Result}; use futures::StreamExt; use orb_connd_events::ConnectionKind; @@ -22,8 +23,13 @@ pub fn spawn( info!("starting net_changed reporter"); task::spawn(async move { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .wrap_err("failed to build reqwest client")?; + loop { - if let Err(e) = report_loop(&nm, &resolved, &zsender).await { + if let Err(e) = report_loop(&nm, &resolved, &zsender, &client).await { error!(error = ?e, "net changed loop error, retrying in {}s. error: {e}", BACKOFF.as_secs()); } @@ -36,6 +42,7 @@ async fn report_loop( nm: &NetworkManager, resolved: &Resolved, zsender: &zenorb::Sender, + client: &reqwest::Client, ) -> Result<()> { let publisher = zsender.publisher("net/changed")?; let mut state_stream = nm.state_stream().await?; @@ -51,7 +58,7 @@ async fn report_loop( .map_err(|e| eyre!("{e}"))?; if is_connected(&conn_event) { - report(nm, resolved, &conn_event).await?; + report(nm, resolved, client, &conn_event).await?; } loop { @@ -74,7 +81,7 @@ async fn report_loop( .map_err(|e| eyre!("{e}"))?; if is_connected(&conn_event) { - report(nm, resolved, &conn_event).await?; + report(nm, resolved, client, &conn_event).await?; } } } @@ -114,6 +121,7 @@ fn is_connected(conn_event: &orb_connd_events::Connection) -> bool { async fn report( nm: &NetworkManager, resolved: &Resolved, + client: &reqwest::Client, conn_event: &orb_connd_events::Connection, ) -> Result<()> { let active_conns = nm.active_connections().await?; @@ -152,7 +160,7 @@ async fn report( } } - match connectivity_check(&connectivity_uri).await { + match connectivity_check(client, &connectivity_uri).await { Ok(check) => { let result = if check.status.is_success() { "ok" @@ -196,11 +204,10 @@ struct ConnectivityCheck { elapsed: Duration, } -async fn connectivity_check(uri: &str) -> Result { - let client = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .timeout(Duration::from_secs(5)) - .build()?; +async fn connectivity_check( + client: &reqwest::Client, + uri: &str, +) -> Result { let start = Instant::now(); let resp = client.get(uri).send().await?; let elapsed = start.elapsed();