clash.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. use super::{PrfEnhancedResult, Profiles, Verge};
  2. use crate::log_if_err;
  3. use crate::utils::{config, dirs, help};
  4. use anyhow::{bail, Result};
  5. use reqwest::header::HeaderMap;
  6. use serde::{Deserialize, Serialize};
  7. use serde_yaml::{Mapping, Value};
  8. use std::{collections::HashMap, time::Duration};
  9. use tauri::api::process::{Command, CommandChild, CommandEvent};
  10. use tauri::Window;
  11. use tokio::time::sleep;
  12. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  13. pub struct ClashInfo {
  14. /// clash sidecar status
  15. pub status: String,
  16. /// clash core port
  17. pub port: Option<String>,
  18. /// same as `external-controller`
  19. pub server: Option<String>,
  20. /// clash secret
  21. pub secret: Option<String>,
  22. }
  23. pub struct Clash {
  24. /// maintain the clash config
  25. pub config: Mapping,
  26. /// some info
  27. pub info: ClashInfo,
  28. /// clash sidecar
  29. pub sidecar: Option<CommandChild>,
  30. /// save the main window
  31. pub window: Option<Window>,
  32. }
  33. impl Clash {
  34. pub fn new() -> Clash {
  35. let config = Clash::read_config();
  36. let info = Clash::get_info(&config);
  37. Clash {
  38. config,
  39. info,
  40. sidecar: None,
  41. window: None,
  42. }
  43. }
  44. /// get clash config
  45. fn read_config() -> Mapping {
  46. config::read_yaml::<Mapping>(dirs::clash_path())
  47. }
  48. /// save the clash config
  49. fn save_config(&self) -> Result<()> {
  50. config::save_yaml(
  51. dirs::clash_path(),
  52. &self.config,
  53. Some("# Default Config For Clash Core\n\n"),
  54. )
  55. }
  56. /// parse the clash's config.yaml
  57. /// get some information
  58. fn get_info(clash_config: &Mapping) -> ClashInfo {
  59. let key_port_1 = Value::String("port".to_string());
  60. let key_port_2 = Value::String("mixed-port".to_string());
  61. let key_server = Value::String("external-controller".to_string());
  62. let key_secret = Value::String("secret".to_string());
  63. let port = match clash_config.get(&key_port_1) {
  64. Some(value) => match value {
  65. Value::String(val_str) => Some(val_str.clone()),
  66. Value::Number(val_num) => Some(val_num.to_string()),
  67. _ => None,
  68. },
  69. _ => None,
  70. };
  71. let port = match port {
  72. Some(_) => port,
  73. None => match clash_config.get(&key_port_2) {
  74. Some(value) => match value {
  75. Value::String(val_str) => Some(val_str.clone()),
  76. Value::Number(val_num) => Some(val_num.to_string()),
  77. _ => None,
  78. },
  79. _ => None,
  80. },
  81. };
  82. let server = match clash_config.get(&key_server) {
  83. Some(value) => match value {
  84. Value::String(val_str) => {
  85. // `external-controller` could be
  86. // "127.0.0.1:9090" or ":9090"
  87. // Todo: maybe it could support single port
  88. let server = val_str.clone();
  89. let server = match server.starts_with(":") {
  90. true => format!("127.0.0.1{server}"),
  91. false => server,
  92. };
  93. Some(server)
  94. }
  95. _ => None,
  96. },
  97. _ => None,
  98. };
  99. let secret = match clash_config.get(&key_secret) {
  100. Some(value) => match value {
  101. Value::String(val_str) => Some(val_str.clone()),
  102. Value::Bool(val_bool) => Some(val_bool.to_string()),
  103. Value::Number(val_num) => Some(val_num.to_string()),
  104. _ => None,
  105. },
  106. _ => None,
  107. };
  108. ClashInfo {
  109. status: "init".into(),
  110. port,
  111. server,
  112. secret,
  113. }
  114. }
  115. /// save the main window
  116. pub fn set_window(&mut self, win: Option<Window>) {
  117. self.window = win;
  118. }
  119. /// run clash sidecar
  120. pub fn run_sidecar(&mut self) -> Result<()> {
  121. let app_dir = dirs::app_home_dir();
  122. let app_dir = app_dir.as_os_str().to_str().unwrap();
  123. match Command::new_sidecar("clash") {
  124. Ok(cmd) => match cmd.args(["-d", app_dir]).spawn() {
  125. Ok((mut rx, cmd_child)) => {
  126. self.sidecar = Some(cmd_child);
  127. // clash log
  128. tauri::async_runtime::spawn(async move {
  129. while let Some(event) = rx.recv().await {
  130. match event {
  131. CommandEvent::Stdout(line) => log::info!("[clash]: {}", line),
  132. CommandEvent::Stderr(err) => log::error!("[clash]: {}", err),
  133. _ => {}
  134. }
  135. }
  136. });
  137. Ok(())
  138. }
  139. Err(err) => bail!(err.to_string()),
  140. },
  141. Err(err) => bail!(err.to_string()),
  142. }
  143. }
  144. /// drop clash sidecar
  145. pub fn drop_sidecar(&mut self) -> Result<()> {
  146. if let Some(sidecar) = self.sidecar.take() {
  147. sidecar.kill()?;
  148. }
  149. Ok(())
  150. }
  151. /// restart clash sidecar
  152. /// should reactivate profile after restart
  153. pub fn restart_sidecar(&mut self, profiles: &mut Profiles) -> Result<()> {
  154. self.update_config();
  155. self.drop_sidecar()?;
  156. self.run_sidecar()?;
  157. self.activate(profiles)?;
  158. self.activate_enhanced(profiles, false, true)
  159. }
  160. /// update the clash info
  161. pub fn update_config(&mut self) {
  162. self.config = Clash::read_config();
  163. self.info = Clash::get_info(&self.config);
  164. }
  165. /// patch update the clash config
  166. pub fn patch_config(
  167. &mut self,
  168. patch: Mapping,
  169. verge: &mut Verge,
  170. profiles: &mut Profiles,
  171. ) -> Result<()> {
  172. let mix_port_key = Value::from("mixed-port");
  173. let mut port = None;
  174. for (key, value) in patch.into_iter() {
  175. let value = value.clone();
  176. // check whether the mix_port is changed
  177. if key == mix_port_key {
  178. if value.is_number() {
  179. port = value.as_i64().as_ref().map(|n| n.to_string());
  180. } else {
  181. port = value.as_str().as_ref().map(|s| s.to_string());
  182. }
  183. }
  184. self.config.insert(key.clone(), value);
  185. }
  186. self.save_config()?;
  187. if let Some(port) = port {
  188. self.restart_sidecar(profiles)?;
  189. verge.init_sysproxy(Some(port));
  190. }
  191. Ok(())
  192. }
  193. /// enable tun mode
  194. /// only revise the config
  195. pub fn tun_mode(&mut self, enable: bool) -> Result<()> {
  196. // Windows 需要wintun.dll文件
  197. #[cfg(target_os = "windows")]
  198. if enable {
  199. let wintun_dll = dirs::app_home_dir().join("wintun.dll");
  200. if !wintun_dll.exists() {
  201. bail!("failed to enable TUN for missing `wintun.dll`");
  202. }
  203. }
  204. macro_rules! revise {
  205. ($map: expr, $key: expr, $val: expr) => {
  206. let ret_key = Value::String($key.into());
  207. $map.insert(ret_key, Value::from($val));
  208. };
  209. }
  210. // if key not exists then append value
  211. macro_rules! append {
  212. ($map: expr, $key: expr, $val: expr) => {
  213. let ret_key = Value::String($key.into());
  214. if !$map.contains_key(&ret_key) {
  215. $map.insert(ret_key, Value::from($val));
  216. }
  217. };
  218. }
  219. // tun config
  220. let tun_val = self.config.get(&Value::from("tun"));
  221. let mut new_tun = Mapping::new();
  222. if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() {
  223. new_tun = tun_val.as_ref().unwrap().as_mapping().unwrap().clone();
  224. }
  225. revise!(new_tun, "enable", enable);
  226. append!(new_tun, "stack", "gvisor");
  227. append!(new_tun, "dns-hijack", vec!["198.18.0.2:53"]);
  228. append!(new_tun, "auto-route", true);
  229. append!(new_tun, "auto-detect-interface", true);
  230. revise!(self.config, "tun", new_tun);
  231. // dns config
  232. let dns_val = self.config.get(&Value::from("dns"));
  233. let mut new_dns = Mapping::new();
  234. if dns_val.is_some() && dns_val.as_ref().unwrap().is_mapping() {
  235. new_dns = dns_val.as_ref().unwrap().as_mapping().unwrap().clone();
  236. }
  237. // 借鉴cfw的默认配置
  238. revise!(new_dns, "enable", enable);
  239. append!(new_dns, "enhanced-mode", "fake-ip");
  240. append!(
  241. new_dns,
  242. "nameserver",
  243. vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
  244. );
  245. append!(new_dns, "fallback", vec![] as Vec<&str>);
  246. #[cfg(target_os = "windows")]
  247. append!(
  248. new_dns,
  249. "fake-ip-filter",
  250. vec![
  251. "dns.msftncsi.com",
  252. "www.msftncsi.com",
  253. "www.msftconnecttest.com"
  254. ]
  255. );
  256. revise!(self.config, "dns", new_dns);
  257. self.save_config()
  258. }
  259. /// activate the profile
  260. /// generate a new profile to the temp_dir
  261. /// then put the path to the clash core
  262. fn _activate(info: ClashInfo, config: Mapping, window: Option<Window>) -> Result<()> {
  263. let temp_path = dirs::profiles_temp_path();
  264. config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
  265. tauri::async_runtime::spawn(async move {
  266. let server = info.server.unwrap();
  267. let server = format!("http://{server}/configs");
  268. let mut headers = HeaderMap::new();
  269. headers.insert("Content-Type", "application/json".parse().unwrap());
  270. if let Some(secret) = info.secret.as_ref() {
  271. let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
  272. headers.insert("Authorization", secret);
  273. }
  274. let mut data = HashMap::new();
  275. data.insert("path", temp_path.as_os_str().to_str().unwrap());
  276. // retry 5 times
  277. for _ in 0..5 {
  278. match reqwest::ClientBuilder::new().no_proxy().build() {
  279. Ok(client) => {
  280. let builder = client.put(&server).headers(headers.clone()).json(&data);
  281. match builder.send().await {
  282. Ok(resp) => {
  283. if resp.status() != 204 {
  284. log::error!("failed to activate clash for status \"{}\"", resp.status());
  285. }
  286. // emit the window to update something
  287. if let Some(window) = window {
  288. window.emit("verge://refresh-clash-config", "yes").unwrap();
  289. }
  290. // do not retry
  291. break;
  292. }
  293. Err(err) => log::error!("failed to activate for `{err}`"),
  294. }
  295. }
  296. Err(err) => log::error!("failed to activate for `{err}`"),
  297. }
  298. sleep(Duration::from_millis(500)).await;
  299. }
  300. });
  301. Ok(())
  302. }
  303. /// enhanced profiles mode
  304. /// - (sync) refresh config if enhance chain is null
  305. /// - (async) enhanced config
  306. pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool, skip: bool) -> Result<()> {
  307. if self.window.is_none() {
  308. bail!("failed to get the main window");
  309. }
  310. let event_name = help::get_uid("e");
  311. let event_name = format!("enhanced-cb-{event_name}");
  312. // generate the payload
  313. let payload = profiles.gen_enhanced(event_name.clone())?;
  314. let info = self.info.clone();
  315. // do not run enhanced
  316. if payload.chain.len() == 0 {
  317. if skip {
  318. return Ok(());
  319. }
  320. let mut config = self.config.clone();
  321. let filter_data = Clash::strict_filter(payload.current);
  322. for (key, value) in filter_data.into_iter() {
  323. config.insert(key, value);
  324. }
  325. return Clash::_activate(info, config, self.window.clone());
  326. }
  327. let window = self.window.clone().unwrap();
  328. let window_move = self.window.clone();
  329. window.once(&event_name, move |event| {
  330. if let Some(result) = event.payload() {
  331. let result: PrfEnhancedResult = serde_json::from_str(result).unwrap();
  332. if let Some(data) = result.data {
  333. let mut config = Clash::read_config();
  334. let filter_data = Clash::loose_filter(data); // loose filter
  335. for (key, value) in filter_data.into_iter() {
  336. config.insert(key, value);
  337. }
  338. log_if_err!(Clash::_activate(info, config, window_move));
  339. log::info!("profile enhanced status {}", result.status);
  340. }
  341. result.error.map(|err| log::error!("{err}"));
  342. }
  343. });
  344. tauri::async_runtime::spawn(async move {
  345. // wait the window setup during resolve app
  346. if delay {
  347. sleep(Duration::from_secs(2)).await;
  348. }
  349. window.emit("script-handler", payload).unwrap();
  350. });
  351. Ok(())
  352. }
  353. /// activate the profile
  354. /// auto activate enhanced profile
  355. pub fn activate(&self, profiles: &Profiles) -> Result<()> {
  356. let data = profiles.gen_activate()?;
  357. let data = Clash::strict_filter(data);
  358. let info = self.info.clone();
  359. let mut config = self.config.clone();
  360. for (key, value) in data.into_iter() {
  361. config.insert(key, value);
  362. }
  363. Clash::_activate(info, config, self.window.clone())
  364. }
  365. /// only 5 default fields available (clash config fields)
  366. /// convert to lowercase
  367. fn strict_filter(config: Mapping) -> Mapping {
  368. // Only the following fields are allowed:
  369. // proxies/proxy-providers/proxy-groups/rule-providers/rules
  370. let valid_keys = vec![
  371. "proxies",
  372. "proxy-providers",
  373. "proxy-groups",
  374. "rules",
  375. "rule-providers",
  376. ];
  377. let mut new_config = Mapping::new();
  378. for (key, value) in config.into_iter() {
  379. key.as_str().map(|key_str| {
  380. // change to lowercase
  381. let mut key_str = String::from(key_str);
  382. key_str.make_ascii_lowercase();
  383. // filter
  384. if valid_keys.contains(&&*key_str) {
  385. new_config.insert(Value::String(key_str), value);
  386. }
  387. });
  388. }
  389. new_config
  390. }
  391. /// more clash config fields available
  392. /// convert to lowercase
  393. fn loose_filter(config: Mapping) -> Mapping {
  394. // all of these can not be revised by script or merge
  395. // http/https/socks port should be under control
  396. let not_allow = vec![
  397. "port",
  398. "socks-port",
  399. "mixed-port",
  400. "allow-lan",
  401. "mode",
  402. "external-controller",
  403. "secret",
  404. "log-level",
  405. ];
  406. let mut new_config = Mapping::new();
  407. for (key, value) in config.into_iter() {
  408. key.as_str().map(|key_str| {
  409. // change to lowercase
  410. let mut key_str = String::from(key_str);
  411. key_str.make_ascii_lowercase();
  412. // filter
  413. if !not_allow.contains(&&*key_str) {
  414. new_config.insert(Value::String(key_str), value);
  415. }
  416. });
  417. }
  418. new_config
  419. }
  420. }
  421. impl Default for Clash {
  422. fn default() -> Self {
  423. Clash::new()
  424. }
  425. }
  426. impl Drop for Clash {
  427. fn drop(&mut self) {
  428. if let Err(err) = self.drop_sidecar() {
  429. log::error!("{err}");
  430. }
  431. }
  432. }