Files
clash-verge-rev/src-tauri/src/feat/clash.rs
T

181 lines
6.3 KiB
Rust

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<Arc<rustls::ClientConfig>> = 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<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))
}