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.
This commit is contained in:
wonfen
2026-08-31 17:09:36 +08:00
parent 8611fdc34f
commit e60285b4ae
8 changed files with 56 additions and 50 deletions
+1 -23
View File
@@ -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<IRuntime>,
}
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);
+39 -7
View File
@@ -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;
}
+1 -1
View File
@@ -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)
-1
View File
@@ -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(())
})
+2 -2
View File
@@ -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" }
+1 -1
View File
@@ -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),
+10 -13
View File
@@ -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) {
@@ -196,12 +196,12 @@ const ProxyControlSwitches = ({
{isTunMode && (
<SwitchRow
label={t('settings.sections.proxyControl.fields.tunMode')}
active={enable_tun_mode || false}
active={(enable_tun_mode && isTunModeAvailable) || false}
infoTitle={t('settings.sections.proxyControl.tooltips.tunMode')}
onInfoClick={() => tunRef.current?.open()}
onToggle={handleTunToggle}
onError={onError}
highlight={enable_tun_mode || false}
highlight={(enable_tun_mode && isTunModeAvailable) || false}
extraIcons={
<>
{!isTunModeAvailable && (