use crate::core::notify::NoticeStatus; use crate::core::notify::{Refresh, announce}; use crate::{ config::{Config, MixedPort}, core::{CoreManager, handle, tray}, feat::clean_async, process::AsyncHandler, utils, }; use bytes::BytesMut; use clash_verge_logging::{Type, logging}; use once_cell::sync::Lazy; use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; use std::sync::Arc; #[allow(clippy::expect_used)] static TLS_CONFIG: Lazy> = Lazy::new(|| { 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() .expect("Failed to set TLS versions") .with_root_certificates(root_store) .with_no_client_auth(); Arc::new(config) }); pub async fn restart_clash_core() { match CoreManager::global().restart_core().await { Ok(_) => { announce(Refresh::Clash); handle::Handle::notice(NoticeStatus::SetConfigOk, "ok"); } Err(err) => { handle::Handle::notice(NoticeStatus::SetConfigError, format!("{err:#}")); logging!(error, Type::Core, "restart core failed: {err:#}"); } } } pub async fn restart_app() { logging!(debug, Type::System, "启动重启应用流程"); handle::Handle::global().set_is_exiting(); Config::apply_all_and_save_file().await; let cleanup_result = clean_async().await; logging!( info, Type::System, "资源清理完成,退出代码: {}", if cleanup_result.all_success { 0 } else { 1 } ); if !cleanup_result.core_stopped { handle::Handle::global().clear_is_exiting(); handle::Handle::notice( NoticeStatus::AppRestartCoreStopFailed, cleanup_result.stop_error.unwrap_or_default(), ); return; } utils::server::shutdown_embedded_server(); let app_handle = handle::Handle::app_handle(); app_handle.restart(); } fn after_change_clash_mode() { AsyncHandler::spawn(move || async { if let Err(err) = handle::Handle::mihomo().close_all_connections().await { logging!( error, Type::Core, "Failed to close all connections after changing clash mode: {err}" ); } }); } /// Propagates mihomo PATCH failures so the frontend can roll back its optimistic mode. pub async fn change_clash_mode(mode: String) -> Result<(), String> { let mut mapping = Mapping::new(); mapping.insert(Value::from("mode"), Value::from(mode.as_str())); let json_value = serde_json::json!({ "mode": mode }); if let Err(err) = handle::Handle::mihomo().patch_base_config(&json_value).await { logging!(error, Type::Core, "change clash mode failed: {err}"); return Err(err.to_string().into()); } let clash = Config::clash().await; clash.edit_draft(|d| d.patch_config(&mapping)); clash.apply(); let clash_data = clash.data_arc(); if clash_data.save_config().await.is_ok() { announce(Refresh::Clash); tray::Tray::global().update_menu_and_icon().await; } let is_auto_close_connection = Config::verge().await.data_arc().auto_close_connection.unwrap_or(false); if is_auto_close_connection { after_change_clash_mode(); } Ok(()) } /// Test delay to a URL through proxy. /// HTTPS: measures TLS handshake time. HTTP: measures HEAD round-trip time. #[tracing::instrument(skip_all, level = "trace", fields(url = %url))] pub async fn test_delay(url: String) -> anyhow::Result { use std::sync::Arc; use std::time::Duration; use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; use tokio::net::TcpStream; use tokio::time::Instant; 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 }); let verge = Config::verge().await.latest_arc(); let proxy_enabled = verge.enable_system_proxy.unwrap_or(false) || verge.enable_tun_mode.unwrap_or(false); let proxy_port = if proxy_enabled { Some(MixedPort::desired().await) } else { None }; tokio::time::timeout(Duration::from_secs(10), async { let start = Instant::now(); let mut buf = BytesMut::with_capacity(1024); if is_https { let stream = match proxy_port { Some(pp) => { let mut s = TcpStream::connect(format!("127.0.0.1:{pp}")).await?; s.write_all(format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n").as_bytes()) .await?; s.read_buf(&mut buf).await?; if !buf.windows(3).any(|w| w == b"200") { return Err(anyhow::anyhow!("Proxy CONNECT failed")); } s } None => TcpStream::connect(format!("{host}:{port}")).await?, }; let connector = tokio_rustls::TlsConnector::from(Arc::clone(&TLS_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?; } else { let (mut stream, req) = match proxy_port { Some(pp) => ( TcpStream::connect(format!("127.0.0.1:{pp}")).await?, format!("HEAD {url} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"), ), None => ( TcpStream::connect(format!("{host}:{port}")).await?, format!("HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"), ), }; stream.write_all(req.as_bytes()).await?; let _ = stream.read(&mut buf).await?; } Ok((start.elapsed().as_millis() as u32).max(1)) }) .await .unwrap_or(Ok(10000u32)) }