pub mod adapter_hw_stats;
pub mod adapters;
pub mod arp;
pub mod bufferbloat;
pub mod captive_portal;
pub mod connection_states;
pub mod connections;
pub mod dhcp;
pub mod dns;
pub mod dns_benchmark;
pub mod dns_cache;
pub mod firewall;
pub mod gateway;
pub mod interfaces;
pub mod ipv6;
pub mod latency;
pub mod listening_ports;
pub mod mtu;
pub mod nat;
pub mod ntp;
pub mod packet_loss;
pub mod ping;
pub mod ports;
pub mod protocol_stats;
pub mod proxy;
pub mod public_ip;
pub mod reverse_dns;
pub mod route_path;
pub mod routing_table;
pub mod shared_cache;
pub mod speed;
pub mod tls_inspection;
pub mod traffic_counters;
pub mod util;
pub mod vpn;
pub mod wifi;
use serde::Serialize;
use crate::config::Config;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum DiagnosticStatus {
Ok,
Warn,
Fail,
Skip,
}
#[derive(Debug, Clone, Serialize)]
pub struct DiagnosticResult {
pub category: String,
pub status: DiagnosticStatus,
pub summary: String,
pub details: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub timed_out: bool,
}
impl DiagnosticResult {
pub fn ok(category: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
category: category.into(),
status: DiagnosticStatus::Ok,
summary: summary.into(),
details: None,
timed_out: false,
}
}
pub fn warn(category: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
category: category.into(),
status: DiagnosticStatus::Warn,
summary: summary.into(),
details: None,
timed_out: false,
}
}
pub fn fail(category: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
category: category.into(),
status: DiagnosticStatus::Fail,
summary: summary.into(),
details: None,
timed_out: false,
}
}
pub fn skip(category: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
category: category.into(),
status: DiagnosticStatus::Skip,
summary: summary.into(),
details: None,
timed_out: false,
}
}
pub fn timed_out_fail(category: impl Into<String>) -> Self {
Self {
category: category.into(),
status: DiagnosticStatus::Fail,
summary: "Timed out — network severely degraded".to_string(),
details: None,
timed_out: true,
}
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DiagnosticResults {
pub timestamp: String,
pub adapters: DiagnosticResult,
pub interfaces: DiagnosticResult,
pub gateway: DiagnosticResult,
pub dns: DiagnosticResult,
pub public_ip: DiagnosticResult,
pub latency: DiagnosticResult,
pub speed: DiagnosticResult,
pub ports: DiagnosticResult,
#[serde(skip_serializing_if = "Option::is_none")]
pub interface_details: Option<Vec<interfaces::InterfaceInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub adapter_details: Option<Vec<adapters::AdapterInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gateway_details: Option<gateway::GatewayInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_details: Option<dns::DnsInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_ip_details: Option<public_ip::PublicIpInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latency_details: Option<Vec<latency::LatencyResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed_details: Option<crate::speedtest::SpeedTestResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port_details: Option<Vec<ports::PortResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub technician: Option<TechnicianResults>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub timed_out: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct TechnicianResults {
#[serde(skip_serializing_if = "Option::is_none")]
pub arp_table: Option<Vec<arp::ArpEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub routing_table: Option<Vec<routing_table::RouteEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_connections: Option<Vec<connections::ConnectionEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub listening_ports: Option<Vec<listening_ports::ListeningPort>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dhcp_info: Option<Vec<dhcp::DhcpLease>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol_stats: Option<protocol_stats::ProtocolStatistics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub adapter_hw_stats: Option<Vec<adapter_hw_stats::AdapterHwStat>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy_config: Option<proxy::ProxyConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vpn_info: Option<Vec<vpn::VpnAdapter>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub firewall_info: Option<firewall::FirewallInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_cache: Option<Vec<dns_cache::DnsCacheEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv6_info: Option<ipv6::Ipv6Info>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtu_info: Option<Vec<mtu::MtuInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection_states: Option<connection_states::ConnectionStates>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bufferbloat: Option<bufferbloat::BufferbloatResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reverse_dns: Option<Vec<reverse_dns::ReverseDnsEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls_inspection: Option<tls_inspection::TlsInspectionResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub traffic_counters: Option<Vec<traffic_counters::TrafficCounter>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub route_path: Option<route_path::RoutePath>,
#[serde(skip_serializing_if = "Option::is_none")]
pub packet_loss: Option<Vec<packet_loss::LossResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nat_analysis: Option<nat::NatAnalysis>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wifi: Option<Vec<wifi::WifiLink>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_benchmark: Option<dns_benchmark::DnsBenchmark>,
#[serde(skip_serializing_if = "Option::is_none")]
pub captive_portal: Option<captive_portal::CaptivePortalResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub clock_sync: Option<ntp::ClockSync>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_mtu: Option<mtu::PathMtu>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arp_health: Option<arp::ArpHealth>,
}
pub fn run_all_cap(config: &Config) -> std::time::Duration {
const RUN_ALL_CAP_FLOOR: std::time::Duration = std::time::Duration::from_secs(90);
const RUN_ALL_CAP_HEADROOM: u64 = 30;
const TECH_DEEP_BUDGET_SECS: u64 = 150;
let base = if config.skip_speed {
RUN_ALL_CAP_FLOOR
} else {
std::time::Duration::from_secs(4 * config.speed_duration + RUN_ALL_CAP_HEADROOM)
.max(RUN_ALL_CAP_FLOOR)
};
if config.is_tech_mode() {
base + std::time::Duration::from_secs(TECH_DEEP_BUDGET_SECS)
} else {
base
}
}
pub async fn run_all(config: &Config, cap: std::time::Duration) -> DiagnosticResults {
let deadline = tokio::time::Instant::now() + cap;
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let mut timed_out = false;
let mut adapters_h = tokio::spawn(adapters::check());
let mut interfaces_h = tokio::spawn(interfaces::check());
let mut gateway_h = tokio::spawn(gateway::check());
let mut dns_h = tokio::spawn(dns::check());
let mut public_ip_h = tokio::spawn(public_ip::check());
let mut latency_h = tokio::spawn(latency::check());
let mut ports_h = tokio::spawn(ports::check());
let completed = tokio::select! {
results = async {
tokio::join!(
&mut adapters_h,
&mut interfaces_h,
&mut gateway_h,
&mut dns_h,
&mut public_ip_h,
&mut latency_h,
&mut ports_h,
)
} => Some(results),
_ = tokio::time::sleep_until(deadline) => None,
};
let (
(adapters_result, adapter_details),
(interfaces_result, interface_details),
(gateway_result, gateway_details),
(dns_result, dns_details),
(public_ip_result, public_ip_details),
(latency_result, latency_details),
(ports_result, port_details),
) = match completed {
Some((a, i, g, d, p, l, po)) => (
a.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Adapters"), Vec::new())),
i.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Network"), Vec::new())),
g.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Gateway"), None)),
d.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("DNS"), None)),
p.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Internet"), None)),
l.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Latency"), Vec::new())),
po.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Ports"), Vec::new())),
),
None => {
timed_out = true;
(
util::harvest_or(
adapters_h,
(DiagnosticResult::timed_out_fail("Adapters"), Vec::new()),
)
.await,
util::harvest_or(
interfaces_h,
(DiagnosticResult::timed_out_fail("Network"), Vec::new()),
)
.await,
util::harvest_or(
gateway_h,
(DiagnosticResult::timed_out_fail("Gateway"), None),
)
.await,
util::harvest_or(dns_h, (DiagnosticResult::timed_out_fail("DNS"), None)).await,
util::harvest_or(
public_ip_h,
(DiagnosticResult::timed_out_fail("Internet"), None),
)
.await,
util::harvest_or(
latency_h,
(DiagnosticResult::timed_out_fail("Latency"), Vec::new()),
)
.await,
util::harvest_or(
ports_h,
(DiagnosticResult::timed_out_fail("Ports"), Vec::new()),
)
.await,
)
}
};
let now = tokio::time::Instant::now();
let (speed_result, speed_details) = if config.skip_speed {
(
DiagnosticResult::skip("Speed", "Speed test skipped (--fast)"),
None,
)
} else if timed_out || now >= deadline {
timed_out = true;
(
DiagnosticResult::skip("Speed", "Skipped — diagnostics timed out"),
None,
)
} else {
match tokio::time::timeout_at(deadline, speed::check(config)).await {
Ok(outcome) => outcome,
Err(_) => {
timed_out = true;
(DiagnosticResult::timed_out_fail("Speed"), None)
}
}
};
let mut adapter_details = adapter_details;
let technician = if config.is_tech_mode() {
if tokio::time::Instant::now() >= deadline {
timed_out = true;
None
} else {
let tech = tokio::time::timeout_at(
deadline,
Box::pin(async {
adapters::enrich_driver_info(&mut adapter_details).await;
run_technician_diagnostics(config, public_ip_details.as_ref()).await
}),
)
.await;
match tech {
Ok(t) => Some(t),
Err(_) => {
timed_out = true;
None
}
}
}
} else {
None
};
DiagnosticResults {
timestamp,
adapters: adapters_result,
interfaces: interfaces_result,
gateway: gateway_result,
dns: dns_result,
public_ip: public_ip_result,
latency: latency_result,
speed: speed_result,
ports: ports_result,
interface_details: Some(interface_details),
adapter_details: Some(adapter_details),
gateway_details,
dns_details,
public_ip_details,
latency_details: Some(latency_details),
speed_details,
port_details: Some(port_details),
technician,
timed_out,
}
}
async fn run_technician_diagnostics(
config: &Config,
public_ip: Option<&public_ip::PublicIpInfo>,
) -> TechnicianResults {
if config.verbose {
eprintln!("[verbose] Running technician deep diagnostics...");
}
let cache = shared_cache::SharedCache::build_for_tech_mode().await;
let (
arp_table,
routing,
conns,
listeners,
dhcp_info,
proto_stats,
hw_stats,
proxy_cfg,
vpn_adapters,
fw_info,
dns_c,
ipv6_i,
mtu_i,
conn_states,
rdns,
tls_insp,
traffic,
wifi_links,
dns_bench,
portal,
clock,
) = tokio::join!(
Box::pin(arp::collect()),
Box::pin(routing_table::collect()),
Box::pin(connections::collect_with_cache(&cache)),
Box::pin(listening_ports::collect_with_cache(&cache)),
Box::pin(dhcp::collect_with_cache(&cache)),
Box::pin(protocol_stats::collect()),
Box::pin(adapter_hw_stats::collect_with_cache(&cache)),
Box::pin(proxy::collect()),
Box::pin(vpn::collect_with_cache(&cache)),
Box::pin(firewall::collect()),
Box::pin(dns_cache::collect()),
Box::pin(ipv6::collect_with_cache(&cache)),
Box::pin(mtu::collect()),
Box::pin(connection_states::collect_with_cache(&cache)),
Box::pin(reverse_dns::collect_with_cache(&cache)),
Box::pin(tls_inspection::collect()),
Box::pin(traffic_counters::collect_with_cache(&cache)),
Box::pin(wifi::collect()),
Box::pin(dns_benchmark::collect()),
Box::pin(captive_portal::collect()),
Box::pin(ntp::collect()),
);
let (route, loss, path_mtu) = tokio::join!(
Box::pin(route_path::collect()),
Box::pin(packet_loss::collect()),
Box::pin(mtu::probe_path_mtu()),
);
let nat_analysis = nat::analyze(
cache.gateway_ip.as_deref(),
public_ip,
route.as_ref().map(|r| r.hops.as_slice()),
);
let arp_health = arp::assess_health(arp_table.as_deref(), cache.gateway_ip.as_deref());
let bufferbloat = Box::pin(bufferbloat::collect(config)).await;
TechnicianResults {
arp_table,
routing_table: routing,
active_connections: conns,
listening_ports: listeners,
dhcp_info,
protocol_stats: proto_stats,
adapter_hw_stats: hw_stats,
proxy_config: proxy_cfg,
vpn_info: vpn_adapters,
firewall_info: fw_info,
dns_cache: dns_c,
ipv6_info: ipv6_i,
mtu_info: mtu_i,
connection_states: conn_states,
bufferbloat,
reverse_dns: rdns,
tls_inspection: tls_insp,
traffic_counters: traffic,
route_path: route,
packet_loss: loss,
nat_analysis,
wifi: wifi_links,
dns_benchmark: dns_bench,
captive_portal: portal,
clock_sync: clock,
path_mtu,
arp_health,
}
}