From 7ae3b7b0de20cf76f8eafc5864cdf70a4505187b Mon Sep 17 00:00:00 2001 From: wonfen Date: Tue, 17 Mar 2026 02:33:28 +0800 Subject: [PATCH] feat: use real TLS latency for website testing --- Cargo.lock | 4 ++ src-tauri/Cargo.toml | 3 ++ src-tauri/src/feat/clash.rs | 87 ++++++++++++++++++++++++------------- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8540ee73d..d06690220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d614cad27..0c930f1fd 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" } diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index b7fe4d686..fd4bad2e1 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -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 { - 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), } }