use std::time::{Duration, Instant};
use colored::*;
use serde::Serialize;
use crate::i18n::{t, t1, t2};
use crate::output::{print_json, OutputMode};
#[derive(Serialize, Clone)]
pub struct DiagItem {
pub check: String,
pub ok: bool,
pub warning: bool,
pub message: String,
}
#[derive(Serialize)]
pub struct DiagReport {
pub timestamp: String,
pub items: Vec<DiagItem>,
pub elapsed_secs: f64,
}
pub async fn run(mode: OutputMode) {
let start = Instant::now();
let (egress, dns_cn, dns_global, gateway, proxy, http_cn, http_global, ipv6) = tokio::join!(
check_egress(),
check_dns_single("baidu.com"),
check_dns_single("google.com"),
check_gateway(),
async { check_proxy_status() },
check_http_single("https://www.baidu.com"),
check_http_single("https://www.google.com"),
check_ipv6(),
);
let mut items = Vec::new();
items.push(egress);
items.push(dns_cn);
items.push(dns_global);
items.push(gateway);
items.push(proxy);
items.push(http_cn);
items.push(http_global);
items.push(ipv6);
let elapsed = start.elapsed();
let timestamp = current_timestamp();
let report = DiagReport {
timestamp: timestamp.clone(),
items: items.clone(),
elapsed_secs: elapsed.as_secs_f64(),
};
if mode == OutputMode::Json {
print_json(&report);
return;
}
println!();
println!("{} {}", t("diag.title").bold(), timestamp.cyan());
println!();
for item in &items {
let symbol = if item.ok && !item.warning {
"✅"
} else if item.warning {
"⚠️ "
} else {
"❌"
};
let colored = if item.ok && !item.warning {
symbol.green()
} else if item.warning {
symbol.yellow()
} else {
symbol.red()
};
println!(" {} [{}] {}", colored, item.check, item.message);
}
println!();
println!(" {}", t("diag.elapsed").replace("{0}", &format!("{:.1}", elapsed.as_secs_f64())));
}
async fn check_egress() -> DiagItem {
let interfaces = crate::info::get_all_interfaces();
let egress_ip = crate::info::egress::detect_egress_ip();
let egress_iface = egress_ip.and_then(|ip| crate::info::egress::find_egress_interface(&ip, &interfaces));
match (egress_iface, egress_ip) {
(Some(name), Some(ip)) => {
let iface = interfaces.iter().find(|i| i.name == name);
let iftype = iface
.map(|i| crate::info::interface::classify_interface(&i.description, &i.name).to_label())
.unwrap_or_default();
DiagItem {
check: t("diag.check_egress"),
ok: true,
warning: false,
message: t2("diag.net_ok", &name, &format!("({}) {}", iftype, ip)),
}
}
_ => DiagItem {
check: t("diag.check_egress"),
ok: false,
warning: false,
message: t("diag.net_fail"),
},
}
}
async fn check_dns_single(domain: &str) -> DiagItem {
use trust_dns_resolver::config::*;
use trust_dns_resolver::TokioAsyncResolver;
let is_cn = domain == "baidu.com";
let check_label = if is_cn { "diag.dns_cn" } else { "diag.dns_global" };
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
let start = Instant::now();
match resolver.lookup_ip(domain).await {
Ok(ips) => {
if let Some(ip) = ips.iter().next() {
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
let msg = t("diag.dns_ok")
.replace("{0}", domain)
.replace("{1}", &ip.to_string())
.replace("{2}", &format!("{:.0}", elapsed));
DiagItem {
check: t(check_label),
ok: true,
warning: false,
message: msg,
}
} else {
DiagItem {
check: t(check_label),
ok: false,
warning: false,
message: t1("diag.dns_fail", domain),
}
}
}
Err(e) => DiagItem {
check: t(check_label),
ok: false,
warning: false,
message: t1("diag.dns_fail", &e.to_string()),
},
}
}
async fn check_gateway() -> DiagItem {
let routes = crate::info::get_default_routes();
if routes.is_empty() {
return DiagItem {
check: t("diag.check_gateway"),
ok: false,
warning: false,
message: t("diag.gw_fail"),
};
}
let gw_ip = &routes[0].0;
let gw_addr: std::net::IpAddr = match gw_ip.parse() {
Ok(ip) => ip,
Err(_) => return DiagItem {
check: t("diag.check_gateway"),
ok: false,
warning: false,
message: t("diag.gw_fail"),
},
};
use surge_ping::{Client, ConfigBuilder, PingIdentifier, PingSequence};
let client = match Client::new(&ConfigBuilder::default().build()) {
Ok(c) => c,
Err(_) => return DiagItem {
check: t("diag.check_gateway"),
ok: true,
warning: true,
message: t1("diag.gw_ok_no_rtt", gw_ip),
},
};
let mut pinger = client.pinger(gw_addr, PingIdentifier(0)).await;
match pinger.ping(PingSequence(0), &[0u8; 32]).await {
Ok((_, rtt)) => {
let ms = rtt.as_secs_f64() * 1000.0;
DiagItem {
check: t("diag.check_gateway"),
ok: true,
warning: false,
message: t2("diag.gw_ok", gw_ip, &format!("{:.1}", ms)),
}
}
Err(_) => DiagItem {
check: t("diag.check_gateway"),
ok: false,
warning: false,
message: t("diag.gw_fail"),
},
}
}
fn check_proxy_status() -> DiagItem {
let proxies = crate::info::proxy::get_proxy_info();
let sys_label = t("proxy.system");
let disabled = t("proxy.disabled");
let env_label = t("proxy.env");
let not_set = t("common.not_set");
let system_proxy = proxies.iter().find(|p| {
p.ptype == sys_label && p.value != disabled
});
let env_proxy = proxies.iter().find(|p| {
p.ptype != sys_label && p.ptype != env_label && p.value != not_set
});
let proxy_value = system_proxy
.or(env_proxy)
.map(|p| p.value.clone());
match proxy_value {
Some(val) => DiagItem {
check: t("diag.check_proxy"),
ok: true,
warning: true,
message: t1("diag.proxy_on", &val),
},
None => DiagItem {
check: t("diag.check_proxy"),
ok: true,
warning: false,
message: t("diag.proxy_off"),
},
}
}
async fn check_http_single(url: &str) -> DiagItem {
let is_cn = url.contains("baidu.com");
let check_label = if is_cn { "diag.http_cn" } else { "diag.http_global" };
let timeout_secs = 5;
let proxy_addr = crate::util::get_system_proxy_addr();
let via_proxy = proxy_addr.is_some();
let client = if let Some(ref proxy_url) = proxy_addr {
reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.proxy(reqwest::Proxy::all(proxy_url).unwrap_or_else(|_| {
reqwest::Proxy::all("http://0.0.0.0:0").unwrap()
}))
.build()
.unwrap_or_else(|_| {
reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.unwrap()
})
} else {
reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.no_proxy()
.build()
.unwrap()
};
let start = Instant::now();
match client.get(url).send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
let proxy_tag = if via_proxy {
t("diag.via_proxy")
} else {
t("diag.direct")
};
let msg = format!(
"{} [{}]",
t("diag.http_ok")
.replace("{0}", url)
.replace("{1}", &status.to_string())
.replace("{2}", &format!("{:.0}", elapsed)),
proxy_tag
);
DiagItem {
check: t(check_label),
ok: true,
warning: false,
message: msg,
}
}
Err(e) => {
let proxy_tag = if via_proxy {
t("diag.via_proxy")
} else {
t("diag.direct")
};
let msg = format!(
"{} [{}]",
t1("diag.http_fail", &e.to_string()),
proxy_tag
);
DiagItem {
check: t(check_label),
ok: false,
warning: false,
message: msg,
}
}
}
}
async fn check_ipv6() -> DiagItem {
use trust_dns_resolver::config::*;
use trust_dns_resolver::TokioAsyncResolver;
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
match resolver.ipv6_lookup("baidu.com").await {
Ok(ips) => {
if ips.iter().next().is_some() {
DiagItem {
check: t("diag.check_ipv6"),
ok: true,
warning: false,
message: t("diag.ipv6_ok"),
}
} else {
DiagItem {
check: t("diag.check_ipv6"),
ok: false,
warning: false,
message: t("diag.ipv6_fail"),
}
}
}
Err(_) => DiagItem {
check: t("diag.check_ipv6"),
ok: false,
warning: false,
message: t("diag.ipv6_fail"),
},
}
}
fn current_timestamp() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let days = secs / 86400;
let hour = (secs % 86400) / 3600;
let min = (secs % 3600) / 60;
let sec = secs % 60;
let (year, month, day) = days_to_date(days as i64);
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, min, sec)
}
fn days_to_date(days: i64) -> (i64, u32, u32) {
let mut year = 1970i64;
let mut remaining = days;
loop {
let days_in_year = if is_leap(year) { 366 } else { 365 };
if remaining < days_in_year {
break;
}
remaining -= days_in_year;
year += 1;
}
let month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut month = 1u32;
let mut day = remaining as u32 + 1;
for (i, &md) in month_days.iter().enumerate() {
let md = if i == 1 && is_leap(year) { 29 } else { md };
if day <= md {
month = (i + 1) as u32;
break;
}
day -= md;
}
(year, month, day)
}
fn is_leap(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}