feat: use real TLS latency for website testing

This commit is contained in:
wonfen 2026-03-17 02:33:28 +08:00
parent 4ceb7e6043
commit 7ae3b7b0de
No known key found for this signature in database
GPG Key ID: CEAFD6C73AB2001F
3 changed files with 65 additions and 29 deletions

4
Cargo.lock generated
View File

@ -1165,6 +1165,7 @@ dependencies = [
"reqwest_dav",
"runas",
"rust_iso3166",
"rustls",
"scopeguard",
"serde",
"serde_json",
@ -1189,8 +1190,10 @@ dependencies = [
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tokio",
"tokio-rustls",
"tokio-stream",
"warp",
"webpki-roots",
"windows 0.62.2",
"winreg 0.56.0",
"zip 8.2.0",
@ -6315,6 +6318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",

View File

@ -102,6 +102,9 @@ clash_verge_service_ipc = { version = "2.1.3", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.8.2"
tokio-rustls = "0.26"
rustls = { version = "0.23", features = ["ring"] }
webpki-roots = "1.0"
rust_iso3166 = "0.1.14"
# Use the git repo until the next release after v2.0.0.
dark-light = { git = "https://github.com/rust-dark-light/dark-light" }

View File

@ -102,40 +102,69 @@ pub async fn change_clash_mode(mode: String) {
}
}
/// Test connection delay to a URL
/// Test TLS handshake delay to a URL (through proxy when not in TUN mode)
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
use crate::utils::network::{NetworkManager, ProxyType};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::net::TcpStream;
use tokio::time::Instant;
let tun_mode = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
let parsed = tauri::Url::parse(&url)?;
let is_https = parsed.scheme() == "https";
let host = parsed
.host_str()
.ok_or_else(|| anyhow::anyhow!("Invalid URL: no host"))?
.to_string();
let port = parsed.port().unwrap_or(if is_https { 443 } else { 80 });
// 如果是TUN模式不使用代理否则使用自身代理
let proxy_type = if !tun_mode {
ProxyType::Localhost
} else {
ProxyType::None
let verge = Config::verge().await.latest_arc();
let tun_mode = verge.enable_tun_mode.unwrap_or(false);
let inner = async {
let start = Instant::now();
// TCP connect: direct in TUN mode, via CONNECT tunnel otherwise
let stream = if tun_mode {
TcpStream::connect(format!("{host}:{port}")).await?
} else {
let proxy_port = match verge.verge_mixed_port {
Some(p) => p,
None => Config::clash().await.data_arc().get_mixed_port(),
};
let mut stream = TcpStream::connect(format!("127.0.0.1:{proxy_port}")).await?;
stream
.write_all(format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n").as_bytes())
.await?;
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).await?;
if !std::str::from_utf8(&buf[..n]).unwrap_or("").contains("200") {
return Err(anyhow::anyhow!("Proxy CONNECT failed"));
}
stream
};
// TLS handshake for HTTPS; for HTTP the TCP connect above is the result
if is_https {
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config =
rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
let server_name = rustls::pki_types::ServerName::try_from(host.as_str())
.map_err(|_| anyhow::anyhow!("Invalid DNS name: {host}"))?
.to_owned();
connector.connect(server_name, stream).await?;
}
// Ensure at least 1ms — frontend treats 0 as timeout
Ok((start.elapsed().as_millis() as u32).max(1))
};
let user_agent = Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0".into());
let start = Instant::now();
let response = NetworkManager::new()
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
.await;
match response {
Ok(response) => {
logging!(trace, Type::Network, "test_delay response: {response:#?}");
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
Ok(10000u32)
}
}
Err(err) => {
logging!(trace, Type::Network, "test_delay error: {err:#?}");
Err(err)
}
match tokio::time::timeout(Duration::from_secs(10), inner).await {
Ok(result) => result,
Err(_) => Ok(10000u32),
}
}