mod chain; pub mod field; mod merge; mod script; pub mod seq; mod tun; use self::{ chain::{AsyncChainItemFrom as _, ChainItem, ChainType}, field::{use_keys, use_lowercase, use_sort}, merge::use_merge, script::use_script, seq::{SeqMap, use_seq}, tun::use_tun, }; use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; use crate::{config::IVerge, constants}; use clash_verge_logging::{Type, logging}; use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use tokio::fs; type ResultLog = Vec<(String, String)>; #[derive(Debug)] struct ConfigValues { clash_config: Mapping, clash_core: Option, enable_tun: bool, enable_builtin: bool, socks_enabled: bool, http_enabled: bool, enable_dns_settings: bool, #[cfg(not(target_os = "windows"))] redir_enabled: bool, #[cfg(target_os = "linux")] tproxy_enabled: bool, } #[derive(Debug)] struct ProfileItems { config: Mapping, merge_item: ChainItem, script_item: ChainItem, rules_item: ChainItem, proxies_item: ChainItem, groups_item: ChainItem, global_merge: ChainItem, global_script: ChainItem, profile_name: String, } impl Default for ProfileItems { fn default() -> Self { Self { config: Default::default(), profile_name: Default::default(), merge_item: ChainItem { uid: "".into(), data: ChainType::Merge(Mapping::new()), }, script_item: ChainItem { uid: "".into(), data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), }, rules_item: ChainItem { uid: "".into(), data: ChainType::Rules(SeqMap::default()), }, proxies_item: ChainItem { uid: "".into(), data: ChainType::Proxies(SeqMap::default()), }, groups_item: ChainItem { uid: "".into(), data: ChainType::Groups(SeqMap::default()), }, global_merge: ChainItem { uid: "Merge".into(), data: ChainType::Merge(Mapping::new()), }, global_script: ChainItem { uid: "Script".into(), data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), }, } } } async fn get_config_values() -> ConfigValues { let clash = Config::clash().await; let clash_arc = clash.latest_arc(); let clash_config = clash_arc.0.clone(); drop(clash_arc); drop(clash); let verge = Config::verge().await; let verge_arc = verge.latest_arc(); let IVerge { ref enable_tun_mode, ref enable_builtin_enhanced, ref verge_socks_enabled, ref verge_http_enabled, ref enable_dns_settings, .. } = **verge_arc; 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), enable_builtin_enhanced.unwrap_or(true), verge_socks_enabled.unwrap_or(false), verge_http_enabled.unwrap_or(false), enable_dns_settings.unwrap_or(false), ); #[cfg(not(target_os = "windows"))] let redir_enabled = verge_arc.verge_redir_enabled.unwrap_or(false); #[cfg(target_os = "linux")] let tproxy_enabled = verge_arc.verge_tproxy_enabled.unwrap_or(false); drop(verge_arc); drop(verge); ConfigValues { clash_config, clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings, #[cfg(not(target_os = "windows"))] redir_enabled, #[cfg(target_os = "linux")] tproxy_enabled, } } #[allow(clippy::cognitive_complexity)] async fn collect_profile_items() -> ProfileItems { let profiles = Config::profiles().await; let profiles_arc = profiles.latest_arc(); drop(profiles); let current = profiles_arc.current_mapping().await.unwrap_or_default(); let current_profile_uid = match profiles_arc.get_current() { Some(uid) => uid, None => { drop(profiles_arc); return ProfileItems::default(); } }; let current_item = match profiles_arc.get_item(current_profile_uid) { Ok(item) => item, Err(_) => { drop(profiles_arc); return ProfileItems::default(); } }; let merge_uid: Cow<'_, str> = if let Some(s) = current_item.current_merge() { Cow::Borrowed(s) } else { Cow::Owned("Merge".into()) }; let script_uid: Cow<'_, str> = if let Some(s) = current_item.current_script() { Cow::Borrowed(s) } else { Cow::Owned("Script".into()) }; let rules_uid: Cow<'_, str> = if let Some(s) = current_item.current_rules() { Cow::Borrowed(s) } else { Cow::Owned("Rules".into()) }; let proxies_uid: Cow<'_, str> = if let Some(s) = current_item.current_proxies() { Cow::Borrowed(s) } else { Cow::Owned("Proxies".into()) }; let groups_uid: Cow<'_, str> = if let Some(s) = current_item.current_groups() { Cow::Borrowed(s) } else { Cow::Owned("Groups".into()) }; let name = profiles_arc .get_item(current_profile_uid) .ok() .and_then(|item| item.name.clone()) .unwrap_or_default(); let merge_item = { let item = profiles_arc.get_item(&merge_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { None } } .unwrap_or_else(|| ChainItem { uid: "".into(), data: ChainType::Merge(Mapping::new()), }); let script_item = { let item = profiles_arc.get_item(&script_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { None } } .unwrap_or_else(|| ChainItem { uid: "".into(), data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), }); let rules_item = { let item = profiles_arc.get_item(&rules_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { None } } .unwrap_or_else(|| ChainItem { uid: "".into(), data: ChainType::Rules(SeqMap::default()), }); let proxies_item = { let item = profiles_arc.get_item(&proxies_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { None } } .unwrap_or_else(|| ChainItem { uid: "".into(), data: ChainType::Proxies(SeqMap::default()), }); let groups_item = { let item = profiles_arc.get_item(&groups_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { None } } .unwrap_or_else(|| ChainItem { uid: "".into(), data: ChainType::Groups(SeqMap::default()), }); let global_merge = { let item = profiles_arc.get_item("Merge").ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { None } } .unwrap_or_else(|| ChainItem { uid: "Merge".into(), data: ChainType::Merge(Mapping::new()), }); let global_script = { let item = profiles_arc.get_item("Script").ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { None } } .unwrap_or_else(|| ChainItem { uid: "Script".into(), data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), }); drop(profiles_arc); ProfileItems { config: current, merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script, profile_name: name, } } fn process_global_items( mut config: Mapping, global_merge: ChainItem, global_script: ChainItem, profile_name: &String, ) -> (Mapping, Vec, HashMap) { let mut result_map = HashMap::new(); let mut exists_keys = use_keys(&config); if let ChainType::Merge(merge) = global_merge.data { exists_keys.extend(use_keys(&merge)); config = use_merge(&merge, config.to_owned()); } if let ChainType::Script(script) = global_script.data { let mut logs = vec![]; match use_script(script, &config, profile_name) { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); config = res_config; logs.extend(res_logs); } Err(err) => logs.push(("exception".into(), err.to_string().into())), } result_map.insert(global_script.uid, logs); } (config, exists_keys, result_map) } #[allow(clippy::too_many_arguments)] fn process_profile_items( mut config: Mapping, mut exists_keys: Vec, mut result_map: HashMap, rules_item: ChainItem, proxies_item: ChainItem, groups_item: ChainItem, merge_item: ChainItem, script_item: ChainItem, profile_name: &String, ) -> (Mapping, Vec, HashMap) { if let ChainType::Rules(rules) = rules_item.data { config = use_seq(rules, config.to_owned(), "rules"); } if let ChainType::Proxies(proxies) = proxies_item.data { config = use_seq(proxies, config.to_owned(), "proxies"); } if let ChainType::Groups(groups) = groups_item.data { config = use_seq(groups, config.to_owned(), "proxy-groups"); } if let ChainType::Merge(merge) = merge_item.data { exists_keys.extend(use_keys(&merge)); config = use_merge(&merge, config.to_owned()); } if let ChainType::Script(script) = script_item.data { let mut logs = vec![]; match use_script(script, &config, profile_name) { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); config = res_config; logs.extend(res_logs); } Err(err) => logs.push(("exception".into(), err.to_string().into())), } result_map.insert(script_item.uid, logs); } (config, exists_keys, result_map) } async fn merge_default_config( mut config: Mapping, clash_config: Mapping, socks_enabled: bool, http_enabled: bool, #[cfg(not(target_os = "windows"))] redir_enabled: bool, #[cfg(target_os = "linux")] tproxy_enabled: bool, ) -> Mapping { for (key, value) in clash_config.into_iter() { if key.as_str() == Some("tun") { let mut tun = config.get_mut("tun").map_or_else(Mapping::new, |val| { val.as_mapping().cloned().unwrap_or_else(Mapping::new) }); let patch_tun = value.as_mapping().cloned().unwrap_or_else(Mapping::new); for (key, value) in patch_tun.into_iter() { tun.insert(key, value); } config.insert("tun".into(), tun.into()); } else { if key.as_str() == Some("socks-port") && !socks_enabled { config.remove("socks-port"); continue; } if key.as_str() == Some("port") && !http_enabled { config.remove("port"); continue; } #[cfg(target_os = "windows")] { if key.as_str() == Some("redir-port") { continue; } } #[cfg(not(target_os = "windows"))] { if key.as_str() == Some("redir-port") && !redir_enabled { config.remove("redir-port"); continue; } } #[cfg(target_os = "linux")] { if key.as_str() == Some("tproxy-port") && !tproxy_enabled { config.remove("tproxy-port"); continue; } } #[cfg(not(target_os = "linux"))] { if key.as_str() == Some("tproxy-port") { config.remove("tproxy-port"); continue; } } // 处理 external-controller 键的开关逻辑 if key.as_str() == Some("external-controller") { let enable_external_controller = Config::verge() .await .latest_arc() .enable_external_controller .unwrap_or(false); if enable_external_controller { config.insert(key, value); } else { // 如果禁用了外部控制器,设置为空字符串 config.insert(key, "".into()); } } else { config.insert(key, value); } } } config } fn apply_builtin_scripts( mut config: Mapping, clash_core: Option, enable_builtin: bool, ) -> Mapping { if enable_builtin { ChainItem::builtin() .into_iter() .filter(|(s, _)| s.is_support(clash_core.as_ref())) .map(|(_, c)| c) .for_each(|item| { logging!(debug, Type::Core, "run builtin script {}", item.uid); if let ChainType::Script(script) = item.data { match use_script(script, &config, &String::from("")) { Ok((res_config, _)) => { config = res_config; } Err(err) => { logging!(error, Type::Core, "builtin script error `{err}`"); } } } }); } config } fn cleanup_proxy_groups(mut config: Mapping) -> Mapping { const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; let proxy_names = config .get("proxies") .and_then(|v| v.as_sequence()) .map(|seq| { seq.iter() .filter_map(|item| match item { Value::Mapping(map) => map .get("name") .and_then(Value::as_str) .map(|name| name.to_owned().into()), Value::String(name) => Some(name.to_owned().into()), _ => None, }) .collect::>() }) .unwrap_or_default(); let group_names = config .get("proxy-groups") .and_then(|v| v.as_sequence()) .map(|seq| { seq.iter() .filter_map(|item| { item.as_mapping() .and_then(|map| map.get("name")) .and_then(Value::as_str) .map(std::convert::Into::into) }) .collect::>() }) .unwrap_or_default(); let provider_names = config .get("proxy-providers") .and_then(Value::as_mapping) .map(|map| { map.keys() .filter_map(Value::as_str) .map(std::convert::Into::into) .collect::>() }) .unwrap_or_default(); let mut allowed_names = proxy_names; allowed_names.extend(group_names); allowed_names.extend(provider_names.iter().cloned()); allowed_names.extend(BUILTIN_POLICIES.iter().map(|p| (*p).into())); if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") { for group in groups { if let Some(group_map) = group.as_mapping_mut() { let mut has_valid_provider = false; if let Some(Value::Sequence(uses)) = group_map.get_mut("use") { uses.retain(|provider| match provider { Value::String(name) => { let exists = provider_names.contains(name.as_str()); has_valid_provider = has_valid_provider || exists; exists } _ => false, }); } if let Some(Value::Sequence(proxies)) = group_map.get_mut("proxies") { proxies.retain(|proxy| match proxy { Value::String(name) => { allowed_names.contains(name.as_str()) || has_valid_provider } _ => true, }); } } } } config } async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping { if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() { let dns_path = app_dir.join(constants::files::DNS_CONFIG); if dns_path.exists() && let Ok(dns_yaml) = fs::read_to_string(&dns_path).await && let Ok(dns_config) = serde_yaml_ng::from_str::(&dns_yaml) { if let Some(hosts_value) = dns_config.get("hosts") && hosts_value.is_mapping() { config.insert("hosts".into(), hosts_value.clone()); logging!(info, Type::Core, "apply hosts configuration"); } if let Some(dns_value) = dns_config.get("dns") { if let Some(dns_mapping) = dns_value.as_mapping() { config.insert("dns".into(), dns_mapping.clone().into()); logging!(info, Type::Core, "apply dns_config.yaml (dns section)"); } } else { config.insert("dns".into(), dns_config.into()); logging!(info, Type::Core, "apply dns_config.yaml"); } } } config } /// Enhance mode /// 返回最终订阅、该订阅包含的键、和script执行的结果 pub async fn enhance() -> (Mapping, Vec, HashMap) { // gather config values let cfg_vals = get_config_values().await; let ConfigValues { clash_config, clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings, #[cfg(not(target_os = "windows"))] redir_enabled, #[cfg(target_os = "linux")] tproxy_enabled, } = cfg_vals; // collect profile items let profile = collect_profile_items().await; let config = profile.config; let merge_item = profile.merge_item; let script_item = profile.script_item; let rules_item = profile.rules_item; let proxies_item = profile.proxies_item; let groups_item = profile.groups_item; let global_merge = profile.global_merge; let global_script = profile.global_script; let profile_name = profile.profile_name; // process globals let (config, exists_keys, result_map) = process_global_items(config, global_merge, global_script, &profile_name); // process profile-specific items let (config, exists_keys, result_map) = process_profile_items( config, exists_keys, result_map, rules_item, proxies_item, groups_item, merge_item, script_item, &profile_name, ); // merge default clash config let config = merge_default_config( config, clash_config, socks_enabled, http_enabled, #[cfg(not(target_os = "windows"))] redir_enabled, #[cfg(target_os = "linux")] tproxy_enabled, ) .await; // builtin scripts let mut config = apply_builtin_scripts(config, clash_core, enable_builtin); config = cleanup_proxy_groups(config); config = use_tun(config, enable_tun); config = use_sort(config); // dns settings config = apply_dns_settings(config, enable_dns_settings).await; let mut exists_set = HashSet::new(); exists_set.extend(exists_keys); let exists_keys: Vec = exists_set.into_iter().collect(); (config, exists_keys, result_map) } #[allow(clippy::expect_used)] #[cfg(test)] mod tests { use super::cleanup_proxy_groups; #[test] fn remove_missing_proxies_from_groups() { let config_str = r#" proxies: - name: "alive-node" type: ss proxy-groups: - name: "manual" type: select proxies: - "alive-node" - "missing-node" - "DIRECT" - name: "nested" type: select proxies: - "manual" - "ghost" "#; let mut config: serde_yaml_ng::Mapping = serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); config = cleanup_proxy_groups(config); let groups = config .get("proxy-groups") .and_then(|v| v.as_sequence()) .cloned() .expect("proxy-groups should be a sequence"); let manual_group = groups .iter() .find(|group| { group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") }) .and_then(|group| group.as_mapping()) .expect("manual group should exist"); let manual_proxies = manual_group .get("proxies") .and_then(|v| v.as_sequence()) .expect("manual proxies should be a sequence"); assert_eq!(manual_proxies.len(), 2); assert!( manual_proxies .iter() .any(|p| p.as_str() == Some("alive-node")) ); assert!(manual_proxies.iter().any(|p| p.as_str() == Some("DIRECT"))); let nested_group = groups .iter() .find(|group| { group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("nested") }) .and_then(|group| group.as_mapping()) .expect("nested group should exist"); let nested_proxies = nested_group .get("proxies") .and_then(|v| v.as_sequence()) .expect("nested proxies should be a sequence"); assert_eq!(nested_proxies.len(), 1); assert_eq!(nested_proxies[0].as_str(), Some("manual")); } #[test] fn keep_provider_backed_groups_intact() { let config_str = r#" proxy-providers: providerA: type: http url: https://example.com path: ./providerA.yaml proxies: [] proxy-groups: - name: "manual" type: select use: - "providerA" - "ghostProvider" proxies: - "dynamic-node" - "DIRECT" "#; let mut config: serde_yaml_ng::Mapping = serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); config = cleanup_proxy_groups(config); let groups = config .get("proxy-groups") .and_then(|v| v.as_sequence()) .cloned() .expect("proxy-groups should be a sequence"); let manual_group = groups .iter() .find(|group| { group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") }) .and_then(|group| group.as_mapping()) .expect("manual group should exist"); let uses = manual_group .get("use") .and_then(|v| v.as_sequence()) .expect("use should be a sequence"); assert_eq!(uses.len(), 1); assert_eq!(uses[0].as_str(), Some("providerA")); let proxies = manual_group .get("proxies") .and_then(|v| v.as_sequence()) .expect("proxies should be a sequence"); assert_eq!(proxies.len(), 2); assert!(proxies.iter().any(|p| p.as_str() == Some("dynamic-node"))); assert!(proxies.iter().any(|p| p.as_str() == Some("DIRECT"))); } #[test] fn prune_invalid_provider_and_proxies_without_provider() { let config_str = r#" proxy-groups: - name: "manual" type: select use: - "ghost-provider" proxies: - "ghost-node" - "DIRECT" "#; let mut config: serde_yaml_ng::Mapping = serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); config = cleanup_proxy_groups(config); let groups = config .get("proxy-groups") .and_then(|v| v.as_sequence()) .cloned() .expect("proxy-groups should be a sequence"); let manual_group = groups .iter() .find(|group| { group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") }) .and_then(|group| group.as_mapping()) .expect("manual group should exist"); let uses = manual_group .get("use") .and_then(|v| v.as_sequence()) .expect("use should be a sequence"); assert_eq!(uses.len(), 0); let proxies = manual_group .get("proxies") .and_then(|v| v.as_sequence()) .expect("proxies should be a sequence"); assert_eq!(proxies.len(), 1); assert_eq!(proxies[0].as_str(), Some("DIRECT")); } }