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::constants; use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; use clash_verge_logging::{Type, logging}; use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; 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 = { Config::clash().await.latest_arc().0.clone() }; let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = { let verge = Config::verge().await; let verge = verge.latest_arc(); ( Some(verge.get_valid_clash_core()), verge.enable_tun_mode.unwrap_or(false), verge.enable_builtin_enhanced.unwrap_or(true), verge.verge_socks_enabled.unwrap_or(false), verge.verge_http_enabled.unwrap_or(false), verge.enable_dns_settings.unwrap_or(false), ) }; #[cfg(not(target_os = "windows"))] let redir_enabled = { let verge = Config::verge().await; let verge = verge.latest_arc(); verge.verge_redir_enabled.unwrap_or(false) }; #[cfg(target_os = "linux")] let tproxy_enabled = { let verge = Config::verge().await; let verge = verge.latest_arc(); verge.verge_tproxy_enabled.unwrap_or(false) }; 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 { // 从profiles里拿东西 - 先收集需要的数据,然后释放锁 let (current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name) = { let current = { let profiles = Config::profiles().await; let profiles_clone = profiles.latest_arc(); profiles_clone.current_mapping().await.unwrap_or_default() }; let profiles = Config::profiles().await; let profiles_ref = profiles.latest_arc(); let current_profile_uid = match profiles_ref.get_current() { Some(uid) => uid.clone(), None => return ProfileItems::default(), }; let current_item = match profiles_ref.get_item_arc(¤t_profile_uid) { Some(item) => item, None => return ProfileItems::default(), }; let merge_uid = current_item .current_merge() .unwrap_or_else(|| "Merge".into()); let script_uid = current_item .current_script() .unwrap_or_else(|| "Script".into()); let rules_uid = current_item .current_rules() .unwrap_or_else(|| "Rules".into()); let proxies_uid = current_item .current_proxies() .unwrap_or_else(|| "Proxies".into()); let groups_uid = current_item .current_groups() .unwrap_or_else(|| "Groups".into()); let name = profiles_ref .get_item(¤t_profile_uid) .ok() .and_then(|item| item.name.clone()) .unwrap_or_default(); ( current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name, ) }; // 现在获取具体的items,此时profiles锁已经释放 let merge_item = { let item = { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); profiles.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 = { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); profiles.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 = { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); profiles.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 = { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); profiles.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 = { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); profiles.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 = { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); profiles.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 = { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); profiles.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()), }); 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.to_owned(), 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.to_owned(), 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.to_owned(), "".into()) { 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.clone()); // 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")); } }