use crate::utils::{dirs, help}; use anyhow::Result; use serde::{Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, }; #[derive(Default, Debug, Clone)] pub struct IClashTemp(pub Mapping); impl IClashTemp { pub fn new() -> Self { let template = Self::template(); match dirs::clash_path().and_then(|path| help::read_mapping(&path)) { Ok(mut map) => { template.0.keys().for_each(|key| { if !map.contains_key(key) { map.insert(key.clone(), template.0.get(key).unwrap().clone()); } }); Self(Self::guard(map)) } Err(err) => { log::error!(target: "app", "{err}"); template } } } pub fn template() -> Self { let mut map = Mapping::new(); let mut tun = Mapping::new(); tun.insert("stack".into(), "gvisor".into()); tun.insert("device".into(), "Mihomo".into()); tun.insert("auto-route".into(), true.into()); tun.insert("strict-route".into(), false.into()); tun.insert("auto-detect-interface".into(), true.into()); tun.insert("dns-hijack".into(), vec!["any:53"].into()); tun.insert("mtu".into(), 1500.into()); #[cfg(not(target_os = "windows"))] map.insert("redir-port".into(), 7895.into()); #[cfg(target_os = "linux")] map.insert("tproxy-port".into(), 7896.into()); map.insert("mixed-port".into(), 7897.into()); map.insert("socks-port".into(), 7898.into()); map.insert("port".into(), 7899.into()); map.insert("log-level".into(), "info".into()); map.insert("allow-lan".into(), false.into()); map.insert("mode".into(), "rule".into()); map.insert("external-controller".into(), "127.0.0.1:9097".into()); map.insert("secret".into(), "".into()); map.insert("tun".into(), tun.into()); Self(map) } fn guard(mut config: Mapping) -> Mapping { #[cfg(not(target_os = "windows"))] let redir_port = Self::guard_redir_port(&config); #[cfg(target_os = "linux")] let tproxy_port = Self::guard_tproxy_port(&config); let mixed_port = Self::guard_mixed_port(&config); let socks_port = Self::guard_socks_port(&config); let port = Self::guard_port(&config); let ctrl = Self::guard_server_ctrl(&config); #[cfg(not(target_os = "windows"))] config.insert("redir-port".into(), redir_port.into()); #[cfg(target_os = "linux")] config.insert("tproxy-port".into(), tproxy_port.into()); config.insert("mixed-port".into(), mixed_port.into()); config.insert("socks-port".into(), socks_port.into()); config.insert("port".into(), port.into()); config.insert("external-controller".into(), ctrl.into()); config } pub fn patch_config(&mut self, patch: Mapping) { for (key, value) in patch.into_iter() { self.0.insert(key, value); } } pub fn save_config(&self) -> Result<()> { help::save_yaml( &dirs::clash_path()?, &self.0, Some("# Generated by Clash Verge"), ) } pub fn get_mixed_port(&self) -> u16 { Self::guard_mixed_port(&self.0) } #[allow(unused)] pub fn get_socks_port(&self) -> u16 { Self::guard_socks_port(&self.0) } #[allow(unused)] pub fn get_port(&self) -> u16 { Self::guard_port(&self.0) } pub fn get_client_info(&self) -> ClashInfo { let config = &self.0; ClashInfo { mixed_port: Self::guard_mixed_port(config), socks_port: Self::guard_socks_port(config), port: Self::guard_port(config), server: Self::guard_client_ctrl(config), secret: config.get("secret").and_then(|value| match value { Value::String(val_str) => Some(val_str.clone()), Value::Bool(val_bool) => Some(val_bool.to_string()), Value::Number(val_num) => Some(val_num.to_string()), _ => None, }), } } #[cfg(not(target_os = "windows"))] pub fn guard_redir_port(config: &Mapping) -> u16 { let mut port = config .get("redir-port") .and_then(|value| match value { Value::String(val_str) => val_str.parse().ok(), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) .unwrap_or(7895); if port == 0 { port = 7895; } port } #[cfg(target_os = "linux")] pub fn guard_tproxy_port(config: &Mapping) -> u16 { let mut port = config .get("tproxy-port") .and_then(|value| match value { Value::String(val_str) => val_str.parse().ok(), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) .unwrap_or(7896); if port == 0 { port = 7896; } port } pub fn guard_mixed_port(config: &Mapping) -> u16 { let mut port = config .get("mixed-port") .and_then(|value| match value { Value::String(val_str) => val_str.parse().ok(), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) .unwrap_or(7897); if port == 0 { port = 7897; } port } pub fn guard_socks_port(config: &Mapping) -> u16 { let mut port = config .get("socks-port") .and_then(|value| match value { Value::String(val_str) => val_str.parse().ok(), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) .unwrap_or(7898); if port == 0 { port = 7898; } port } pub fn guard_port(config: &Mapping) -> u16 { let mut port = config .get("port") .and_then(|value| match value { Value::String(val_str) => val_str.parse().ok(), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) .unwrap_or(7899); if port == 0 { port = 7899; } port } pub fn guard_server_ctrl(config: &Mapping) -> String { config .get("external-controller") .and_then(|value| match value.as_str() { Some(val_str) => { let val_str = val_str.trim(); let val = match val_str.starts_with(':') { true => format!("127.0.0.1{val_str}"), false => val_str.to_owned(), }; SocketAddr::from_str(val.as_str()) .ok() .map(|s| s.to_string()) } None => None, }) .unwrap_or("127.0.0.1:9097".into()) } pub fn guard_client_ctrl(config: &Mapping) -> String { let value = Self::guard_server_ctrl(config); match SocketAddr::from_str(value.as_str()) { Ok(mut socket) => { if socket.ip().is_unspecified() { socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); } socket.to_string() } Err(_) => "127.0.0.1:9097".into(), } } } #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct ClashInfo { /// clash core port pub mixed_port: u16, pub socks_port: u16, pub port: u16, /// same as `external-controller` pub server: String, /// clash secret pub secret: Option, } #[test] fn test_clash_info() { fn get_case, D: Into>(mp: T, ec: D) -> ClashInfo { let mut map = Mapping::new(); map.insert("mixed-port".into(), mp.into()); map.insert("external-controller".into(), ec.into()); IClashTemp(IClashTemp::guard(map)).get_client_info() } fn get_result>(port: u16, server: S) -> ClashInfo { ClashInfo { mixed_port: port, socks_port: 7898, port: 7899, server: server.into(), secret: None, } } assert_eq!( IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(), get_result(7897, "127.0.0.1:9097") ); assert_eq!(get_case("", ""), get_result(7897, "127.0.0.1:9097")); assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9097")); assert_eq!( get_case(8888, "127.0.0.1:8888"), get_result(8888, "127.0.0.1:8888") ); assert_eq!( get_case(8888, " :98888 "), get_result(8888, "127.0.0.1:9097") ); assert_eq!( get_case(8888, "0.0.0.0:8080 "), get_result(8888, "127.0.0.1:8080") ); assert_eq!( get_case(8888, "0.0.0.0:8080"), get_result(8888, "127.0.0.1:8080") ); assert_eq!( get_case(8888, "[::]:8080"), get_result(8888, "127.0.0.1:8080") ); assert_eq!( get_case(8888, "192.168.1.1:8080"), get_result(8888, "192.168.1.1:8080") ); assert_eq!( get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097") ); } #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct IClash { pub mixed_port: Option, pub allow_lan: Option, pub log_level: Option, pub ipv6: Option, pub mode: Option, pub external_controller: Option, pub secret: Option, pub dns: Option, pub tun: Option, pub interface_name: Option, } #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct IClashTUN { pub enable: Option, pub stack: Option, pub auto_route: Option, pub auto_detect_interface: Option, pub dns_hijack: Option>, } #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct IClashDNS { pub enable: Option, pub listen: Option, pub default_nameserver: Option>, pub enhanced_mode: Option, pub fake_ip_range: Option, pub use_hosts: Option, pub fake_ip_filter: Option>, pub nameserver: Option>, pub fallback: Option>, pub fallback_filter: Option, pub nameserver_policy: Option>, } #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct IClashFallbackFilter { pub geoip: Option, pub geoip_code: Option, pub ipcidr: Option>, pub domain: Option>, }