From e60285b4ae878021622e07a7654a8476e78cc62d Mon Sep 17 00:00:00 2001 From: wonfen Date: Mon, 31 Aug 2026 17:09:36 +0800 Subject: [PATCH] fix(tun): make the TUN switch reflect what the Core actually runs The switch read enable_tun_mode while the Core was given enable_tun_mode && !session_suppression, so falling back to Sidecar left it showing a TUN that was not running, with nothing to write the flag back. Drop the session flag: the stored preference is the only truth, re-checked at startup and written off when the conditions for it are not met, and the switch renders it against current capability. --- src-tauri/src/config/config.rs | 24 +--------- src-tauri/src/core/manager/lifecycle.rs | 46 ++++++++++++++++--- src-tauri/src/core/runstate/health.rs | 2 +- src-tauri/src/core/service.rs | 1 - src-tauri/src/core/tray/mod.rs | 4 +- src-tauri/src/enhance/mod.rs | 2 +- src-tauri/src/feat/tun.rs | 23 ++++------ .../shared/proxy-control-switches.tsx | 4 +- 8 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index f086f8694..28a1db7c6 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -19,11 +19,7 @@ use clash_verge_draft::Draft; use clash_verge_logging::{Type, logging, logging_error}; use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; -use std::{ - collections::HashSet, - path::PathBuf, - sync::atomic::{AtomicBool, Ordering}, -}; +use std::{collections::HashSet, path::PathBuf}; use tokio::sync::{Mutex, MutexGuard, OnceCell}; use tokio::time::sleep; @@ -34,7 +30,6 @@ pub(crate) struct Config { runtime_config: Draft, } -static TUN_SESSION_SUPPRESSED: AtomicBool = AtomicBool::new(false); static CONFIG_WRITE_LOCK: Mutex<()> = Mutex::const_new(()); impl Config { @@ -82,24 +77,7 @@ impl Config { Ok(()) } - pub fn tun_suppressed_for_session() -> bool { - TUN_SESSION_SUPPRESSED.load(Ordering::Acquire) - } - - pub(crate) async fn suppress_tun_for_session() { - TUN_SESSION_SUPPRESSED.store(true, Ordering::Release); - Handle::refresh_verge(); - let _ = tray::Tray::global().update_menu().await; - } - - pub(crate) async fn restore_tun_for_session() { - TUN_SESSION_SUPPRESSED.store(false, Ordering::Release); - Handle::refresh_verge(); - let _ = tray::Tray::global().update_menu().await; - } - pub(crate) async fn disable_tun_and_persist() -> Result<()> { - TUN_SESSION_SUPPRESSED.store(false, Ordering::Release); let verge = Self::verge().await; verge.edit_draft(|draft| { draft.enable_tun_mode = Some(false); diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 41e048b58..24c453a48 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -320,15 +320,34 @@ impl CoreManager { defer! { self.finish_config_update(); } + // Lock order is config then lifecycle. disable_tun_and_persist commits the shared Verge + // draft without claiming it, so without this it can commit another transaction's staged patch. + let config_write = Config::lock_config_write().await; let _life = self.lifecycle_lock.lock().await; let status = SERVICE_MANAGER.current().await; let mode = self.get_running_mode(); if !can_allow_sidecar_for_session(&mode, &status) { anyhow::bail!("Sidecar continuation is not allowed from {mode:?} / {status:?}"); } - Config::suppress_tun_for_session().await; - Config::generate().await?; SERVICE_MANAGER.allow_sidecar_for_session()?; + // Settling on Sidecar is what makes the verdict final, so ask only once it is recorded. + // Elevation alone carries TUN on Sidecar; a Sidecar that cannot must write it off. + let prepared = async { + let tun_enabled = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false); + if crate::core::runstate::RUN_STATE + .state() + .tun_should_be_disabled(tun_enabled) + { + Config::disable_tun_and_persist().await?; + } + Config::generate().await + } + .await; + drop(config_write); + if let Err(error) = prepared { + SERVICE_MANAGER.withdraw_sidecar_allowance(); + return Err(error); + } // Drop the guard last so an earlier failure cannot leave a running Sidecar unguarded. proxy_control::stop_guard().await; let result = async { @@ -360,6 +379,8 @@ impl CoreManager { defer! { self.finish_config_update(); } + // Lock order is config then lifecycle, for the unclaimed draft write below. + let _config_write = Config::lock_config_write().await; let _life = self.lifecycle_lock.lock().await; self.controlled_stop_core_inner().await?; @@ -371,7 +392,14 @@ impl CoreManager { .await }, || async { - Config::disable_tun_and_persist().await?; + // Must be asked after the uninstall: until then the Service still makes TUN capable. + let tun_enabled = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false); + if crate::core::runstate::RUN_STATE + .state() + .tun_should_be_disabled(tun_enabled) + { + Config::disable_tun_and_persist().await?; + } Config::generate().await }, || async { @@ -597,8 +625,7 @@ impl CoreManager { #[cfg(target_os = "windows")] self.wait_for_service_if_needed().await; - let service_required = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false) - && !Config::tun_suppressed_for_session(); + let service_required = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false); if service_required && matches!(SERVICE_MANAGER.current().await, ServiceStatus::NotInstalled) && SERVICE_MANAGER.require_install_for_session().is_err() @@ -645,8 +672,9 @@ impl CoreManager { use std::sync::atomic::Ordering; use std::time::Instant; + // An accepted Sidecar is the user's decision for this session, not a wait for the Service. let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false) - && !Config::tun_suppressed_for_session(); + && !crate::core::runstate::RUN_STATE.state().sidecar_allowed; if !needs_service { return; } @@ -701,6 +729,10 @@ impl CoreManager { #[cfg(target_os = "windows")] async fn try_handoff_sidecar_to_service(&self) -> HandoffOutcome { + // Before probing: a probe reply records an observation, which clears sidecar_allowed. + if crate::core::runstate::RUN_STATE.state().sidecar_allowed { + return HandoffOutcome::Done; + } if !Self::refresh_service_readiness_for_handoff().await { return HandoffOutcome::NotReady; } @@ -717,7 +749,7 @@ impl CoreManager { if !matches!(*self.get_running_mode(), RunningMode::Sidecar) || !Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false) - || Config::tun_suppressed_for_session() + || crate::core::runstate::RUN_STATE.state().sidecar_allowed { return HandoffOutcome::Done; } diff --git a/src-tauri/src/core/runstate/health.rs b/src-tauri/src/core/runstate/health.rs index 7fc1e9394..6bb3505a0 100644 --- a/src-tauri/src/core/runstate/health.rs +++ b/src-tauri/src/core/runstate/health.rs @@ -95,7 +95,7 @@ impl RunState { !matches!(self.health, ServiceHealth::Unknown) && !self.op_in_flight && !self.service_needs_attention() } - /// Present-but-broken services remain in the repair flow rather than suppressing TUN. + /// Present-but-broken services remain in the repair flow rather than being disabled here. #[must_use] pub const fn startup_tun_should_be_disabled(&self, tun_enabled: bool) -> bool { matches!(self.health, ServiceHealth::NotInstalled) && self.tun_should_be_disabled(tun_enabled) diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 77cc942d9..5514a31f1 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -1327,7 +1327,6 @@ impl ServiceManager { RUN_STATE.perform(action).await?; if !matches!(action, PendingAction::Uninstall) { wait_for_service_ipc().await?; - Config::restore_tun_for_session().await; } Ok(()) }) diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 36d67fe23..8f9f5ba06 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -63,7 +63,7 @@ pub struct Tray { impl TrayState { async fn get_tray_icon(verge: &IVerge) -> (bool, Cow<'_, [u8]>) { - let tun_mode = verge.enable_tun_mode.unwrap_or(false); + let tun_mode = verge.enable_tun_mode.unwrap_or(false) && crate::core::runstate::RUN_STATE.state().tun_capable(); let system_mode = verge.enable_system_proxy.unwrap_or(false); let kind = if tun_mode { IconKind::Tun @@ -284,7 +284,7 @@ impl Tray { let verge = Config::verge().await.latest_arc(); let system_proxy = verge.enable_system_proxy.unwrap_or(false); - let tun_mode = verge.enable_tun_mode.unwrap_or(false); + let tun_mode = verge.enable_tun_mode.unwrap_or(false) && crate::core::runstate::RUN_STATE.state().tun_capable(); let switch_str = |flag: bool| { if flag { "on" } else { "off" } diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 2d26e9c33..ec4464a56 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -124,7 +124,7 @@ async fn get_config_values() -> ConfigValues { let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( Some(verge_arc.get_valid_clash_core()), - enable_tun_mode.unwrap_or(false) && !Config::tun_suppressed_for_session(), + enable_tun_mode.unwrap_or(false), enable_builtin_enhanced.unwrap_or(true), verge_socks_enabled.unwrap_or(false), verge_http_enabled.unwrap_or(false), diff --git a/src-tauri/src/feat/tun.rs b/src-tauri/src/feat/tun.rs index ef9c1a070..eb730d3b9 100644 --- a/src-tauri/src/feat/tun.rs +++ b/src-tauri/src/feat/tun.rs @@ -13,13 +13,10 @@ use crate::{ /// Prevents recursive runtime disable writes. static DISABLING_TUN: AtomicBool = AtomicBool::new(false); -/// Suppress TUN for this Windows session when startup confirms the Service is absent. -/// The saved preference remains available for a later Service install. +/// Turn TUN off when startup confirms the Service is absent, so the Core can start on Sidecar. +/// +/// Runs before the runtime config is generated. pub async fn reconcile_startup_tun_availability() { - if !cfg!(target_os = "windows") { - return; - } - // Read fresh state after prior config writes complete. let _config_write = Config::lock_config_write().await; let tun_enabled = Config::verge().await.data_arc().enable_tun_mode.unwrap_or(false); @@ -30,9 +27,13 @@ pub async fn reconcile_startup_tun_availability() { logging!( info, Type::Setup, - "Windows Service is not installed; suppressing TUN mode so the Core can start on Sidecar" + "Service is not installed; turning TUN mode off so the Core can start on Sidecar" ); - Config::suppress_tun_for_session().await; + // Write the preference only: a patch would reach update_config_checked, which with no Core to + // reload starts one ahead of the rest of startup, and rolls the write back if that fails. + if let Err(error) = Config::disable_tun_and_persist().await { + logging!(error, Type::Setup, "failed to turn TUN mode off at startup: {error:#}"); + } } /// Disable TUN when the current settled Run State cannot support it. @@ -45,15 +46,11 @@ pub async fn reconcile_tun_availability() { let config_write = Config::lock_config_write().await; let state = RUN_STATE.state(); - // Startup uses the narrower session-suppression path above. + // Startup uses the narrower path above, before a Core is running. if matches!(state.mode, RunningMode::NotRunning) { return; } - if Config::tun_suppressed_for_session() { - return; - } - // Legacy writers may still hold an uncommitted draft. let tun_enabled = Config::verge().await.data_arc().enable_tun_mode.unwrap_or(false); if !state.tun_should_be_disabled(tun_enabled) { diff --git a/src/components/shared/proxy-control-switches.tsx b/src/components/shared/proxy-control-switches.tsx index bc920266d..c82d54d25 100644 --- a/src/components/shared/proxy-control-switches.tsx +++ b/src/components/shared/proxy-control-switches.tsx @@ -196,12 +196,12 @@ const ProxyControlSwitches = ({ {isTunMode && ( tunRef.current?.open()} onToggle={handleTunToggle} onError={onError} - highlight={enable_tun_mode || false} + highlight={(enable_tun_mode && isTunModeAvailable) || false} extraIcons={ <> {!isTunModeAvailable && (