Files
clash-verge-rev/src-tauri/src/feat/clash.rs
T
2026-11-04 10:17:52 +08:00

164 lines
5.8 KiB
Rust

use crate::core::notify::NoticeStatus;
use crate::core::notify::{Refresh, announce};
use crate::{
config::{Config, MixedPort},
core::{CoreManager, handle, tray},
feat::run_exit_cleanup_flow,
process::AsyncHandler,
utils,
};
use bytes::BytesMut;
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::sync::{Arc, LazyLock};
#[allow(clippy::expect_used)]
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> =
LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions")));
pub async fn restart_clash_core() {
match CoreManager::global().restart_core().await {
Ok(_) => {
announce(Refresh::Clash);
}
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, "启动重启应用流程");
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await;
if !cleanup_result.core_stopped {
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());
}
if mode.as_str() == "global"
&& let Err(err) = crate::config::profiles::leave_default_global_exit().await
{
logging!(warn, Type::Core, "failed to move GLOBAL off its default: {err:#}");
}
let clash = Config::clash().await;
let save_result = {
let _config_write = Config::lock_config_write().await;
clash.edit_draft(|d| d.patch_config(&mapping));
clash.apply();
clash.data_arc().save_config().await
};
if save_result.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<u32> {
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))
}