mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 10:42:09 +08:00
feat(dns): remember override state per profile
Save the DNS override switch and session confirmation per profile so automatic protection only disables the affected profile. Preserve the legacy switch as the initial preference and show the profile scope beside the settings gear and in the DNS editor. Refs #7993
This commit is contained in:
@@ -133,8 +133,12 @@ pub fn take_dns_override_notice() -> bool {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_dns_override(enabled: bool, confirmation: Option<String>) -> CmdResult<feat::DnsOverrideOutcome> {
|
||||
feat::set_dns_override(enabled, confirmation)
|
||||
pub async fn set_dns_override(
|
||||
profile_uid: String,
|
||||
enabled: bool,
|
||||
confirmation: Option<String>,
|
||||
) -> CmdResult<feat::DnsOverrideOutcome> {
|
||||
feat::set_dns_override(profile_uid, enabled, confirmation)
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
|
||||
}
|
||||
|
||||
+123
-31
@@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use clash_verge_draft::DraftTransaction;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use smartstring::alias::String;
|
||||
@@ -54,6 +55,7 @@ pub(crate) fn dns_override_source(profile_uid: &str, config: &Mapping) -> Result
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct DnsOverrideState {
|
||||
profile_uid: String,
|
||||
pub source: Option<String>,
|
||||
pub enabled: bool,
|
||||
requested: bool,
|
||||
@@ -61,9 +63,10 @@ pub(crate) struct DnsOverrideState {
|
||||
}
|
||||
|
||||
impl DnsOverrideState {
|
||||
pub fn new(source: Option<String>, requested: bool, confirmation: Option<String>) -> Self {
|
||||
pub fn new(profile_uid: &str, source: Option<String>, requested: bool, confirmation: Option<String>) -> Self {
|
||||
let enabled = requested && (source.is_none() || source == confirmation);
|
||||
Self {
|
||||
profile_uid: profile_uid.into(),
|
||||
source,
|
||||
enabled,
|
||||
requested,
|
||||
@@ -72,24 +75,56 @@ impl DnsOverrideState {
|
||||
}
|
||||
|
||||
fn apply_to(&self, verge: &mut IVerge) -> bool {
|
||||
if self.profile_uid.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let settings = verge.dns_settings_for(&self.profile_uid);
|
||||
// A later settings write must not be overwritten by an earlier runtime generation.
|
||||
if verge.enable_dns_settings.unwrap_or(false) != self.requested
|
||||
|| verge.dns_override_confirmation != self.confirmation
|
||||
{
|
||||
if (settings.enabled, settings.confirmation.as_ref()) != (self.requested, self.confirmation.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
let clear_confirmation = self.confirmation.is_some() && self.source != self.confirmation;
|
||||
if self.enabled == self.requested && !clear_confirmation {
|
||||
if self.enabled == self.requested
|
||||
&& !clear_confirmation
|
||||
&& verge.profile_dns_settings.contains_key(&self.profile_uid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
verge.enable_dns_settings = Some(self.enabled);
|
||||
if clear_confirmation {
|
||||
verge.dns_override_confirmation = None;
|
||||
}
|
||||
verge.profile_dns_settings.insert(
|
||||
self.profile_uid.clone(),
|
||||
ProfileDnsSettings {
|
||||
enabled: self.enabled,
|
||||
confirmation: if clear_confirmation {
|
||||
None
|
||||
} else {
|
||||
self.confirmation.clone()
|
||||
},
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct ProfileDnsSettings {
|
||||
pub enabled: bool,
|
||||
// Force-enable confirmation is valid only for the current app session.
|
||||
#[serde(skip)]
|
||||
pub confirmation: Option<String>,
|
||||
}
|
||||
|
||||
impl IVerge {
|
||||
pub(crate) fn dns_settings_for(&self, profile_uid: &str) -> ProfileDnsSettings {
|
||||
self.profile_dns_settings
|
||||
.get(profile_uid)
|
||||
.cloned()
|
||||
.unwrap_or(ProfileDnsSettings {
|
||||
enabled: self.enable_dns_settings.unwrap_or(false),
|
||||
confirmation: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub(crate) async fn sync_dns_override() -> Result<()> {
|
||||
let _config_write = Self::lock_config_write().await;
|
||||
@@ -116,7 +151,7 @@ impl Config {
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
|
||||
mod tests {
|
||||
use super::{DnsOverrideState, dns_override_source};
|
||||
use super::{DnsOverrideState, ProfileDnsSettings, dns_override_source};
|
||||
use crate::config::IVerge;
|
||||
use anyhow::Result;
|
||||
use serde_yaml_ng::Mapping;
|
||||
@@ -152,10 +187,10 @@ mod tests {
|
||||
let reordered =
|
||||
mapping("dns:\n nameserver-policy:\n b.example: [8.8.8.8]\n a.example: 1.1.1.1\nport: 7890");
|
||||
assert_eq!(source, dns_override_source("one", &reordered)?);
|
||||
assert!(DnsOverrideState::new(source.clone(), true, source.clone()).enabled);
|
||||
assert!(!DnsOverrideState::new(dns_override_source("two", &original)?, true, source.clone()).enabled);
|
||||
assert!(DnsOverrideState::new("one", source.clone(), true, source.clone()).enabled);
|
||||
assert!(!DnsOverrideState::new("two", dns_override_source("two", &original)?, true, source.clone()).enabled);
|
||||
let changed = mapping("dns: {nameserver-policy: {a.example: 9.9.9.9, b.example: [8.8.8.8]}}");
|
||||
assert!(!DnsOverrideState::new(dns_override_source("one", &changed)?, true, source).enabled);
|
||||
assert!(!DnsOverrideState::new("one", dns_override_source("one", &changed)?, true, source).enabled);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -163,24 +198,28 @@ mod tests {
|
||||
fn dns_override_confirmation_expires_on_restart() -> Result<()> {
|
||||
let source = Some("provider-dns".into());
|
||||
let confirmed = IVerge {
|
||||
enable_dns_settings: Some(true),
|
||||
dns_override_confirmation: source.clone(),
|
||||
profile_dns_settings: [(
|
||||
"one".into(),
|
||||
ProfileDnsSettings {
|
||||
enabled: true,
|
||||
confirmation: source.clone(),
|
||||
},
|
||||
)]
|
||||
.into(),
|
||||
..IVerge::default()
|
||||
};
|
||||
assert!(DnsOverrideState::new(source.clone(), true, confirmed.dns_override_confirmation.clone()).enabled);
|
||||
let settings = confirmed.dns_settings_for("one");
|
||||
assert!(DnsOverrideState::new("one", source.clone(), settings.enabled, settings.confirmation).enabled);
|
||||
for saved in [
|
||||
serde_yaml_ng::to_string(&confirmed)?,
|
||||
"enable_dns_settings: true\ndns_override_confirmation: provider-dns".to_owned(),
|
||||
] {
|
||||
let mut restarted: IVerge = serde_yaml_ng::from_str(&saved)?;
|
||||
let state = DnsOverrideState::new(
|
||||
source.clone(),
|
||||
restarted.enable_dns_settings.unwrap_or(false),
|
||||
restarted.dns_override_confirmation.clone(),
|
||||
);
|
||||
let settings = restarted.dns_settings_for("one");
|
||||
let state = DnsOverrideState::new("one", source.clone(), settings.enabled, settings.confirmation);
|
||||
assert!(!state.enabled, "a previous session must not bypass startup protection");
|
||||
assert!(state.apply_to(&mut restarted));
|
||||
assert_eq!(restarted.enable_dns_settings, Some(false));
|
||||
assert!(!restarted.dns_settings_for("one").enabled);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -191,21 +230,74 @@ mod tests {
|
||||
enable_dns_settings: Some(true),
|
||||
..IVerge::default()
|
||||
};
|
||||
let state = DnsOverrideState::new(Some("provider-dns".into()), true, None);
|
||||
let state = DnsOverrideState::new("one", Some("provider-dns".into()), true, None);
|
||||
assert!(state.apply_to(&mut verge));
|
||||
assert_eq!(verge.enable_dns_settings, Some(false));
|
||||
assert!(!verge.dns_settings_for("one").enabled);
|
||||
assert!(!state.apply_to(&mut verge));
|
||||
let disabled = DnsOverrideState::new(None, false, None);
|
||||
let disabled = DnsOverrideState::new("one", None, false, None);
|
||||
assert!(!disabled.apply_to(&mut verge));
|
||||
assert!(!disabled.enabled);
|
||||
|
||||
verge.enable_dns_settings = Some(true);
|
||||
verge.dns_override_confirmation = Some("provider-dns".into());
|
||||
verge.profile_dns_settings.insert(
|
||||
"one".into(),
|
||||
ProfileDnsSettings {
|
||||
enabled: true,
|
||||
confirmation: Some("provider-dns".into()),
|
||||
},
|
||||
);
|
||||
assert!(!state.apply_to(&mut verge));
|
||||
assert_eq!(verge.enable_dns_settings, Some(true));
|
||||
assert_eq!(verge.dns_override_confirmation.as_deref(), Some("provider-dns"));
|
||||
let leaving = DnsOverrideState::new(None, true, verge.dns_override_confirmation.clone());
|
||||
let settings = verge.dns_settings_for("one");
|
||||
assert!(settings.enabled);
|
||||
assert_eq!(settings.confirmation.as_deref(), Some("provider-dns"));
|
||||
let leaving = DnsOverrideState::new("one", None, true, settings.confirmation);
|
||||
assert!(leaving.apply_to(&mut verge));
|
||||
assert!(verge.dns_override_confirmation.is_none());
|
||||
assert!(verge.dns_settings_for("one").confirmation.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_disable_is_saved_only_for_the_affected_profile() -> Result<()> {
|
||||
let mut verge = IVerge {
|
||||
enable_dns_settings: Some(true),
|
||||
..IVerge::default()
|
||||
};
|
||||
let first = DnsOverrideState::new("one", None, true, None);
|
||||
assert!(first.apply_to(&mut verge));
|
||||
let protected = DnsOverrideState::new("two", Some("provider-dns".into()), true, None);
|
||||
assert!(protected.apply_to(&mut verge));
|
||||
assert!(verge.dns_settings_for("one").enabled);
|
||||
assert!(!verge.dns_settings_for("two").enabled);
|
||||
assert!(verge.dns_settings_for("unvisited").enabled);
|
||||
assert_eq!(verge.enable_dns_settings, Some(true));
|
||||
let restarted: IVerge = serde_yaml_ng::from_str(&serde_yaml_ng::to_string(&verge)?)?;
|
||||
assert!(restarted.dns_settings_for("one").enabled);
|
||||
assert!(!restarted.dns_settings_for("two").enabled);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_profiles_preserves_each_confirmation_until_its_source_changes() {
|
||||
let mut verge = IVerge::default();
|
||||
for uid in ["one", "two"] {
|
||||
verge.profile_dns_settings.insert(
|
||||
uid.into(),
|
||||
ProfileDnsSettings {
|
||||
enabled: true,
|
||||
confirmation: Some(uid.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
for uid in ["one", "two", "one"] {
|
||||
let settings = verge.dns_settings_for(uid);
|
||||
let state = DnsOverrideState::new(uid, Some(uid.into()), settings.enabled, settings.confirmation);
|
||||
assert!(state.enabled);
|
||||
assert!(!state.apply_to(&mut verge));
|
||||
}
|
||||
let settings = verge.dns_settings_for("two");
|
||||
let updated = DnsOverrideState::new("two", Some("updated".into()), settings.enabled, settings.confirmation);
|
||||
assert!(updated.apply_to(&mut verge));
|
||||
assert!(!verge.dns_settings_for("two").enabled);
|
||||
assert!(verge.dns_settings_for("two").confirmation.is_none());
|
||||
assert!(verge.dns_settings_for("one").enabled);
|
||||
assert_eq!(verge.dns_settings_for("one").confirmation.as_deref(), Some("one"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,12 +80,11 @@ pub struct IVerge {
|
||||
|
||||
pub enable_bypass_check: Option<bool>,
|
||||
|
||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||
/// Initial DNS override preference for profiles without a saved setting.
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
|
||||
// Force-enable confirmation is valid only for the current app session.
|
||||
#[serde(skip)]
|
||||
pub dns_override_confirmation: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub profile_dns_settings: std::collections::BTreeMap<String, super::dns::ProfileDnsSettings>,
|
||||
|
||||
pub use_default_bypass: Option<bool>,
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ async fn chain_item_or_default(item: Option<&PrfItem>, default_item: impl FnOnce
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_config_values() -> ConfigValues {
|
||||
async fn get_config_values(profile_uid: &str) -> ConfigValues {
|
||||
let clash = Config::clash().await;
|
||||
let clash_arc = clash.latest_arc();
|
||||
let clash_config = clash_arc.0.clone();
|
||||
@@ -120,12 +120,12 @@ async fn get_config_values() -> ConfigValues {
|
||||
ref enable_builtin_enhanced,
|
||||
ref verge_socks_enabled,
|
||||
ref verge_http_enabled,
|
||||
ref enable_dns_settings,
|
||||
ref enable_external_controller,
|
||||
..
|
||||
} = **verge_arc;
|
||||
let enable_external_controller = enable_external_controller.unwrap_or(false);
|
||||
let dns_override_confirmation = verge_arc.dns_override_confirmation.clone();
|
||||
let dns_settings = verge_arc.dns_settings_for(profile_uid);
|
||||
let dns_override_confirmation = dns_settings.confirmation;
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
|
||||
Some(verge_arc.get_valid_clash_core()),
|
||||
@@ -133,7 +133,7 @@ async fn get_config_values() -> ConfigValues {
|
||||
enable_builtin_enhanced.unwrap_or(true),
|
||||
verge_socks_enabled.unwrap_or(false),
|
||||
verge_http_enabled.unwrap_or(false),
|
||||
enable_dns_settings.unwrap_or(false),
|
||||
dns_settings.enabled,
|
||||
);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -873,7 +873,8 @@ async fn apply_dns_settings(config: Mapping, enable_dns_settings: bool) -> (Mapp
|
||||
pub async fn enhance(
|
||||
profiles: &IProfiles,
|
||||
) -> Result<(Mapping, HashSet<String>, HashMap<String, ResultLog>, DnsOverrideState)> {
|
||||
let cfg_vals = get_config_values().await;
|
||||
let profile_uid = profiles.current.as_deref().unwrap_or_default();
|
||||
let cfg_vals = get_config_values(profile_uid).await;
|
||||
let ConfigValues {
|
||||
clash_config,
|
||||
clash_core,
|
||||
@@ -892,7 +893,8 @@ pub async fn enhance(
|
||||
|
||||
let profile = collect_profile_items(profiles).await?;
|
||||
let dns_override = DnsOverrideState::new(
|
||||
dns_override_source(profiles.current.as_deref().unwrap_or_default(), &profile.config)?,
|
||||
profile_uid,
|
||||
dns_override_source(profile_uid, &profile.config)?,
|
||||
enable_dns_settings,
|
||||
dns_override_confirmation,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, ensure};
|
||||
use clash_verge_draft::DraftTransaction;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
|
||||
use crate::{
|
||||
config::{Config, dns::dns_override_source, profiles::PROFILE_WRITE_LOCK},
|
||||
config::{
|
||||
Config,
|
||||
dns::{ProfileDnsSettings, dns_override_source},
|
||||
profiles::PROFILE_WRITE_LOCK,
|
||||
},
|
||||
constants::files::DNS_CONFIG,
|
||||
core::{CoreManager, handle::Handle},
|
||||
utils::dirs,
|
||||
@@ -17,10 +21,18 @@ pub enum DnsOverrideOutcome {
|
||||
ConfirmationRequired { source: String },
|
||||
}
|
||||
|
||||
pub async fn set_dns_override(enabled: bool, confirmation: Option<String>) -> Result<DnsOverrideOutcome> {
|
||||
pub async fn set_dns_override(
|
||||
profile_uid: String,
|
||||
enabled: bool,
|
||||
confirmation: Option<String>,
|
||||
) -> Result<DnsOverrideOutcome> {
|
||||
let _profile_write = PROFILE_WRITE_LOCK.lock().await;
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
let profiles = Config::profiles().await.data_arc();
|
||||
ensure!(
|
||||
profiles.current.as_ref() == Some(&profile_uid),
|
||||
"The active profile changed; retry the DNS setting"
|
||||
);
|
||||
let source = dns_override_source(
|
||||
profiles.current.as_deref().unwrap_or_default(),
|
||||
&profiles.current_mapping().await?,
|
||||
@@ -37,8 +49,13 @@ pub async fn set_dns_override(enabled: bool, confirmation: Option<String>) -> Re
|
||||
let verge = Config::verge().await;
|
||||
let transaction = DraftTransaction::begin(vec![&verge])?;
|
||||
verge.edit_draft(|draft| {
|
||||
draft.enable_dns_settings = Some(enabled);
|
||||
draft.dns_override_confirmation = if enabled { source } else { None };
|
||||
draft.profile_dns_settings.insert(
|
||||
profile_uid.clone(),
|
||||
ProfileDnsSettings {
|
||||
enabled,
|
||||
confirmation: if enabled { source } else { None },
|
||||
},
|
||||
);
|
||||
});
|
||||
CoreManager::global().update_config_checked().await?;
|
||||
|
||||
@@ -47,8 +64,9 @@ pub async fn set_dns_override(enabled: bool, confirmation: Option<String>) -> Re
|
||||
// A subscription update can replace the file while Mihomo is validating it.
|
||||
if enabled && let Some(state) = state.filter(|state| !state.enabled) {
|
||||
verge.edit_draft(|draft| {
|
||||
draft.enable_dns_settings = Some(false);
|
||||
draft.dns_override_confirmation = None;
|
||||
draft
|
||||
.profile_dns_settings
|
||||
.insert(profile_uid, ProfileDnsSettings::default());
|
||||
});
|
||||
if let Some(source) = state.source {
|
||||
outcome = DnsOverrideOutcome::ConfirmationRequired { source };
|
||||
|
||||
Reference in New Issue
Block a user