use crate::{ config::{Config, MixedPort}, core::proxy_control::{self, SystemProxyStateUnknown}, singleton, utils::server, }; use anyhow::Result; use clash_verge_logging::{Type, logging}; use parking_lot::RwLock; use scopeguard::defer; use smartstring::alias::String; use std::{ sync::{ Arc, atomic::{AtomicBool, Ordering}, }, time::Duration, }; use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy}; use tokio::sync::Mutex as TokioMutex; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ProxyApplyStep { Sysproxy, Autoproxy, } const fn proxy_apply_steps(sys_enabled: bool, auto_enabled: bool) -> [ProxyApplyStep; 2] { // Disabling PAC clears WinINET proxy flags on Windows, so pure global // proxy mode must clear PAC before enabling Sysproxy. if sys_enabled && !auto_enabled { [ProxyApplyStep::Autoproxy, ProxyApplyStep::Sysproxy] } else { [ProxyApplyStep::Sysproxy, ProxyApplyStep::Autoproxy] } } /// Maximum guard drain time before OS state is unknown. const GUARD_DRAIN_TIMEOUT: Duration = Duration::from_secs(10); /// Authoritative OS state after a failed proxy write. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum AuthoritativeState { /// No write is known to have landed. Unchanged, /// A partial write left no known target. Unknown, } /// Choose state from reliable evidence that a write landed. fn authoritative_state(error: &anyhow::Error, earlier_step_completed: bool) -> AuthoritativeState { // Linux setter success is not reliable write evidence. authoritative_state_from(!cfg!(target_os = "linux"), error, earlier_step_completed) } /// Testable form with explicit write-evidence reliability. fn authoritative_state_from( write_evidence_is_reliable: bool, error: &anyhow::Error, earlier_step_completed: bool, ) -> AuthoritativeState { if !write_evidence_is_reliable { return AuthoritativeState::Unchanged; } // A completed earlier setter is evidence outside the current error. if earlier_step_completed { return AuthoritativeState::Unknown; } match error.downcast_ref::() { Some(sysproxy::Error::ProxyWrite { progress, .. }) if !progress.nothing_written() => { AuthoritativeState::Unknown } _ => AuthoritativeState::Unchanged, } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum OsProxyState { AlreadyApplied, DifferentOrUnknown, } #[cfg(any(target_os = "macos", test))] fn target_is_already_in_place(snapshot: &sysproxy::ProxySnapshot, sys: &Sysproxy, auto: &Autoproxy) -> bool { if auto.enable { // PAC writes bypass state too. snapshot.matches_pac(auto) && snapshot.bypass_matches(&sys.bypass) } else if sys.enable { snapshot.matches_global(sys) } else { // Bypass state is inert while every proxy is disabled. snapshot.is_all_disabled() } } /// Treat failed reads as different so writes are skipped only with proof. fn classify_os_proxy_state(snapshot: Result) -> OsProxyState { match snapshot { Ok(true) => OsProxyState::AlreadyApplied, _ => OsProxyState::DifferentOrUnknown, } } #[cfg(target_os = "macos")] fn read_os_proxy_state(sys: &Sysproxy, auto: &Autoproxy) -> OsProxyState { // Use the per-protocol snapshot rather than the synthesized proxy view. let already_applied = Sysproxy::snapshot() .map(|snapshot| target_is_already_in_place(&snapshot, sys, auto)) .map_err(anyhow::Error::from); if let Err(err) = &already_applied { logging!(warn, Type::Core, "failed to read OS proxy snapshot: {err:#}"); } classify_os_proxy_state(already_applied) } #[cfg(not(target_os = "macos"))] fn read_os_proxy_state(_sys: &Sysproxy, _auto: &Autoproxy) -> OsProxyState { classify_os_proxy_state(Err(anyhow::anyhow!("per-protocol proxy snapshots are macOS only"))) } /// Disable, drain a racing guard if needed, then disable again. async fn disable_until_the_last_write_is_ours( drained: bool, disable: Disable, drain_again: Drain, ) -> Result<()> where Disable: Fn() -> DisableFuture, DisableFuture: std::future::Future>, Drain: FnOnce() -> DrainFuture, DrainFuture: std::future::Future, { disable().await?; if drained { return Ok(()); } // The final disable should follow the last in-flight guard write, but a guard that will not // stop must not keep the proxy on: disable again either way. if !drain_again().await { logging!( warn, Type::Core, "the system proxy guard did not finish within {GUARD_DRAIN_TIMEOUT:?}; disabling anyway" ); } disable().await } /// Return the first failure after logging every attempted write. fn first_failure(attempts: [(&'static str, Result<()>); N]) -> Result<()> { let mut first: Option = None; for (what, outcome) in attempts { let Err(error) = outcome else { continue }; logging!(warn, Type::Core, "failed to turn the {what} off: {error:#}"); if first.is_none() { first = Some(error); } } first.map_or(Ok(()), Err) } /// Attempt to disable both proxy kinds even if the first fails. fn disable_both(disable_global: Global, disable_pac: Pac) -> Result<()> where Global: FnOnce() -> Result<()>, Pac: FnOnce() -> Result<()>, { let global = disable_global(); let pac = disable_pac(); first_failure([("global proxy", global), ("PAC", pac)]) } /// Treats a missing network service as a completed disable: with nothing to write to, the /// requested "no proxy" state already holds. /// /// Enabling still fails: the guard is opt-in and nothing else re-applies the proxy when a /// service appears, so a skipped enable would claim a proxy the OS never received. /// /// Reports whether the write reached the OS, so recovery does not mistake a skip for evidence. fn skip_without_network_service(enabling: bool, outcome: sysproxy::Result<()>) -> Result { match outcome { Err(error) if !enabling && proxy_control::is_missing_network_service(&error) => { logging!(warn, Type::Core, "no active network service, nothing to turn off"); Ok(false) } other => other.map(|()| true).map_err(anyhow::Error::from), } } /// Force both proxy kinds off, in one blocking hop. async fn disable_all_proxies(sys: Sysproxy, auto: Autoproxy) -> Result<()> { tokio::task::spawn_blocking(move || { disable_both( || skip_without_network_service(sys.enable, sys.set_system_proxy()).map(|_reached_os| ()), || skip_without_network_service(auto.enable, auto.set_auto_proxy()).map(|_reached_os| ()), ) }) .await? } /// Reconcile OS and guard state while preserving the original failure. async fn recover_from_failed_write( error: anyhow::Error, earlier_step_completed: bool, recover_guard: Guard, compensate: Compensate, ) -> anyhow::Error where Guard: FnOnce(AuthoritativeState), Compensate: FnOnce() -> CompensateFuture, CompensateFuture: std::future::Future>, { let state = authoritative_state(&error, earlier_step_completed); recover_guard(state); match state { AuthoritativeState::Unchanged => error, AuthoritativeState::Unknown => { if let Err(compensation) = compensate().await { logging!( warn, Type::Core, "failed to force the system proxy off after a partial write: {compensation:#}" ); } error.context(SystemProxyStateUnknown) } } } async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState { tokio::task::spawn_blocking(move || read_os_proxy_state(&sys, &auto)) .await .unwrap_or_else(|join_error| { logging!(warn, Type::Core, "failed to read OS proxy state: {join_error}"); OsProxyState::DifferentOrUnknown }) } pub(crate) struct Sysopt { update_lock: TokioMutex<()>, guard_operation_lock: TokioMutex<()>, reset_sysproxy: AtomicBool, inner_proxy: Arc>, guard: Arc>, } impl Default for Sysopt { fn default() -> Self { Self { update_lock: TokioMutex::new(()), guard_operation_lock: TokioMutex::new(()), reset_sysproxy: AtomicBool::new(false), inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))), guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), } } } #[cfg(target_os = "windows")] static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;*.local;"; #[cfg(target_os = "windows")] static BYPASS_SEPARATOR: &str = ";"; #[cfg(target_os = "linux")] static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1"; #[cfg(any(target_os = "linux", target_os = "macos"))] static BYPASS_SEPARATOR: &str = ","; #[cfg(target_os = "macos")] static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; fn format_bypass(use_default: bool, custom_bypass: &str) -> String { if custom_bypass.is_empty() { DEFAULT_BYPASS.into() } else if use_default { format!("{DEFAULT_BYPASS}{BYPASS_SEPARATOR}{custom_bypass}").into() } else { custom_bypass.into() } } async fn get_bypass() -> String { let verge = Config::verge().await.latest_arc(); let use_default = verge.use_default_bypass.unwrap_or(true); let custom_bypass = verge.system_proxy_bypass.as_deref().unwrap_or(""); format_bypass(use_default, custom_bypass) } singleton!(Sysopt, SYSOPT); impl Sysopt { fn new() -> Self { Self::default() } fn access_guard(&self) -> Arc> { Arc::clone(&self.guard) } /// Stop the guard and return whether it drained. async fn stop_proxy_guard_locked(&self) -> bool { // Drop the parking_lot read guard before awaiting. let idle = self.access_guard().read().shutdown(); let drained = idle.wait_timeout(GUARD_DRAIN_TIMEOUT).await; if !drained { logging!( warn, Type::Core, "the system proxy guard did not finish within {GUARD_DRAIN_TIMEOUT:?}" ); } drained } /// Stop the guard before handing OS proxy ownership elsewhere. pub(super) async fn stop_proxy_guard(&self) -> bool { let _operation = self.guard_operation_lock.lock().await; self.stop_proxy_guard_locked().await } /// Reconcile guard state with configuration and report success. pub(super) async fn refresh_guard(&self) -> bool { logging!(info, Type::Core, "Refreshing system proxy guard..."); let verge = Config::verge().await.latest_arc(); let _operation = self.guard_operation_lock.lock().await; if !verge.enable_system_proxy.unwrap_or_default() { logging!(info, Type::Core, "System proxy is disabled."); let _drained = self.stop_proxy_guard_locked().await; return true; } if !verge.enable_proxy_guard.unwrap_or_default() { logging!(info, Type::Core, "System proxy guard is disabled."); let _drained = self.stop_proxy_guard_locked().await; return true; } logging!( info, Type::Core, "Updating system proxy with duration: {} seconds", verge.proxy_guard_duration.unwrap_or(30) ); { let guard = self.access_guard(); guard .write() .set_interval(Duration::from_secs(verge.proxy_guard_duration.unwrap_or(30))); } logging!(info, Type::Core, "Starting system proxy guard..."); { let guard = self.access_guard(); if !guard.read().start() { logging!( warn, Type::Core, "the system proxy guard refused to start; a previous run has not finished" ); } } while self.access_guard().read().get_state().is_pendding() { tokio::task::yield_now().await; } // `start()` false may also mean a guard was already running. !self.access_guard().read().get_state().is_stopped() } /// Recover OS and guard state after a failed write. async fn recover_from_failed_write( &self, error: anyhow::Error, earlier_step_completed: bool, guard_was_running: bool, off: (Sysproxy, Autoproxy), ) -> anyhow::Error { recover_from_failed_write( error, earlier_step_completed, |state| self.recover_guard_after_failure(state, guard_was_running), || disable_all_proxies(off.0, off.1), ) .await } /// Reconcile the guard with the authoritative state. fn recover_guard_after_failure(&self, state: AuthoritativeState, was_running: bool) { match state { AuthoritativeState::Unchanged if was_running => { let restarted = self.access_guard().read().start(); if !restarted { logging!( warn, Type::Core, "the system proxy guard refused to start again after a failed write; it is not running" ); } } AuthoritativeState::Unchanged => {} AuthoritativeState::Unknown => { self.access_guard().write().set_guard_type(GuardType::None); } } } /// Wait for any in-progress `update_sysproxy` to finish, so that a /// subsequent read of OS-level sysproxy state sees a fully applied /// configuration instead of a partially-applied one (e.g. SOCKS already /// disabled but HTTP still enabled mid-transition). pub(crate) async fn wait_idle(&self) { let _ = self.update_lock.lock().await; } /// init the sysproxy pub(super) async fn update_sysproxy(&self) -> Result<()> { let _lock = self.update_lock.lock().await; let verge = Config::verge().await.latest_arc(); // Configured, not live: this runs while the Core is being started or restarted, and // asking a Core that is not up yet would only fall back here anyway. let port = MixedPort::desired().await; let pac_port = server::embedded_server_port()?; // 先 await, 避免持有锁导致的 Send 问题 let bypass = get_bypass().await; let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( verge.enable_system_proxy.unwrap_or_default(), verge.proxy_auto_config.unwrap_or_default(), verge.proxy_host.as_deref().unwrap_or("127.0.0.1"), verge.enable_proxy_guard.unwrap_or_default(), ); let (sys, auto, guard_type) = { let (sys, auto) = &mut *self.inner_proxy.write(); sys.host = proxy_host.into(); sys.port = port; sys.bypass = bypass.into(); auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); // `enable_system_proxy` is the master switch. // When disabled, force clear both global proxy and PAC at OS level. let guard_type = if !sys_enable { sys.enable = false; auto.enable = false; GuardType::None } else if pac_enable { sys.enable = false; auto.enable = true; if proxy_guard { GuardType::Autoproxy(auto.clone()) } else { GuardType::None } } else { sys.enable = true; auto.enable = false; if proxy_guard { GuardType::Sysproxy(sys.clone()) } else { GuardType::None } }; (sys.clone(), auto.clone(), guard_type) }; let _guard_operation = self.guard_operation_lock.lock().await; // Drain the guard before any OS read or write. let guard_was_running = !self.access_guard().read().get_state().is_stopped(); let idle = self.access_guard().read().shutdown(); let drained = idle.wait_timeout(GUARD_DRAIN_TIMEOUT).await; // A slow guard does not stop the write, but it does make the OS read untrustworthy. if cfg!(target_os = "macos") && drained && current_os_proxy_state(sys.clone(), auto.clone()).await == OsProxyState::AlreadyApplied { self.access_guard().write().set_guard_type(guard_type); return Ok(()); } let apply_steps = proxy_apply_steps(sys.enable, auto.enable); // Prepare the disabled state used to compensate a partial write. let compensation = { let (mut off_sys, mut off_auto) = (sys.clone(), auto.clone()); off_sys.enable = false; off_auto.enable = false; (off_sys, off_auto) }; // Only a step that actually wrote is evidence the OS changed; a skipped one is not. let applied = tokio::task::spawn_blocking(move || { let mut earlier_step_reached_os = false; for step in apply_steps { let written = match step { ProxyApplyStep::Autoproxy => skip_without_network_service(auto.enable, auto.set_auto_proxy()), ProxyApplyStep::Sysproxy => skip_without_network_service(sys.enable, sys.set_system_proxy()), }; match written { Ok(reached_os) => earlier_step_reached_os |= reached_os, Err(error) => return Err((earlier_step_reached_os, error)), } } Ok(()) }) .await; match applied { Ok(Ok(())) => {} Ok(Err((earlier_step_completed, error))) => { return Err(self .recover_from_failed_write(error, earlier_step_completed, guard_was_running, compensation) .await); } Err(join_error) => { let error = anyhow::Error::from(join_error).context("the system proxy write task did not finish"); return Err(self .recover_from_failed_write(error, false, guard_was_running, compensation) .await); } } // Never point the guard at a target that failed to reach the OS. self.access_guard().write().set_guard_type(guard_type); Ok(()) } /// reset the sysproxy pub(super) async fn reset_sysproxy(&self) -> Result<()> { if self .reset_sysproxy .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_err() { return Ok(()); } defer! { self.reset_sysproxy.store(false, Ordering::SeqCst); } let _lock = self.update_lock.lock().await; let _guard_operation = self.guard_operation_lock.lock().await; let drained = self.stop_proxy_guard_locked().await; // 直接关闭所有代理 let (sys, auto) = { let (sys, auto) = &mut *self.inner_proxy.write(); sys.enable = false; auto.enable = false; (sys.clone(), auto.clone()) }; // The match is trustworthy only after the guard drained. if cfg!(target_os = "macos") && drained && current_os_proxy_state(sys.clone(), auto.clone()).await == OsProxyState::AlreadyApplied { return Ok(()); } disable_until_the_last_write_is_ours( drained, || disable_all_proxies(sys.clone(), auto.clone()), || self.stop_proxy_guard_locked(), ) .await } } #[cfg(test)] mod tests { use super::{ AuthoritativeState, BYPASS_SEPARATOR, DEFAULT_BYPASS, OsProxyState, ProxyApplyStep, SystemProxyStateUnknown, authoritative_state, authoritative_state_from, classify_os_proxy_state, disable_both, disable_until_the_last_write_is_ours, first_failure, format_bypass, proxy_apply_steps, recover_from_failed_write, target_is_already_in_place, }; use parking_lot::Mutex; use std::collections::VecDeque; use sysproxy::{Autoproxy, Sysproxy}; #[cfg(target_os = "macos")] use super::skip_without_network_service; #[cfg(target_os = "macos")] #[test] fn a_machine_with_no_network_service_has_nothing_to_turn_off() { for missing in [ sysproxy::Error::NoActiveNetworkService, sysproxy::Error::NetworkInterface, ] { assert_eq!(skip_without_network_service(false, Err(missing)).ok(), Some(false)); } assert_eq!(skip_without_network_service(false, Ok(())).ok(), Some(true)); assert!(skip_without_network_service(false, Err(sysproxy::Error::RequiresAdminPrivileges)).is_err()); } /// Enabling the global proxy disables PAC first; that skipped step must not look like a write. #[cfg(target_os = "macos")] #[test] fn a_skipped_step_is_not_evidence_that_the_os_was_touched() { let skipped = skip_without_network_service(false, Err(sysproxy::Error::NoActiveNetworkService)); let earlier_step_reached_os = skipped.unwrap_or(true); assert!(!earlier_step_reached_os); let failed = skip_without_network_service(true, Err(sysproxy::Error::NoActiveNetworkService)) .err() .unwrap_or_else(|| anyhow::anyhow!("enabling without a network service must fail")); assert_eq!( authoritative_state(&failed, earlier_step_reached_os), AuthoritativeState::Unchanged ); } #[cfg(target_os = "macos")] #[test] fn turning_the_proxy_on_without_a_network_service_still_fails() { assert!(skip_without_network_service(true, Err(sysproxy::Error::NoActiveNetworkService)).is_err()); assert!(skip_without_network_service(true, Err(sysproxy::Error::NetworkInterface)).is_err()); } #[test] fn empty_custom_bypass_uses_defaults() { assert_eq!(format_bypass(false, ""), DEFAULT_BYPASS); } #[test] fn custom_bypass_can_replace_defaults() { assert_eq!(format_bypass(false, "example.com"), "example.com"); } #[test] fn default_and_custom_bypass_use_platform_separator() { assert_eq!( format_bypass(true, "example.com"), format!("{DEFAULT_BYPASS}{BYPASS_SEPARATOR}example.com") ); } #[cfg(target_os = "windows")] #[test] fn windows_default_and_custom_bypass_use_semicolon() { assert_eq!( format_bypass(true, "example.com"), format!("{DEFAULT_BYPASS};example.com") ); } async fn record_disable_sequence( drained: bool, second_drain: bool, disable_answers: &[bool], ) -> (Vec<&'static str>, bool) { let calls = Mutex::new(Vec::new()); let remaining = Mutex::new(disable_answers.iter().copied().collect::>()); let result = disable_until_the_last_write_is_ours( drained, || { calls.lock().push("disable"); let succeeds = remaining.lock().pop_front().unwrap_or(true); async move { if succeeds { Ok(()) } else { anyhow::bail!("networksetup refused") } } }, || { calls.lock().push("drain"); async move { second_drain } }, ) .await; (calls.into_inner(), result.is_ok()) } fn failure_text(result: anyhow::Result<()>) -> std::string::String { result.map_or_else(|error| format!("{error:#}"), |()| "no failure".to_owned()) } #[test] fn every_proxy_kind_is_attempted_even_after_one_of_them_failed() { let attempted = Mutex::new(Vec::new()); let failed = disable_both( || { attempted.lock().push("global proxy"); Err(anyhow::anyhow!("global refused")) }, || { attempted.lock().push("PAC"); Err(anyhow::anyhow!("PAC refused")) }, ); assert_eq!(&*attempted.lock(), &["global proxy", "PAC"]); assert_eq!(failure_text(failed), "global refused"); } #[test] fn a_later_failure_is_still_reported_when_the_first_one_succeeded() { let failed = first_failure([("global proxy", Ok(())), ("PAC", Err(anyhow::anyhow!("PAC refused")))]); assert_eq!(failure_text(failed), "PAC refused"); } #[test] fn nothing_is_reported_when_every_kind_went_through() { assert!(first_failure([("global proxy", Ok(())), ("PAC", Ok(()))]).is_ok()); } struct Recovery { guard_told: Option, compensated: bool, state_unknown: bool, #[cfg(not(target_os = "linux"))] message: String, } async fn record_recovery(error: anyhow::Error, earlier_step_completed: bool, compensation_works: bool) -> Recovery { let guard_told = Mutex::new(None); let compensated = Mutex::new(false); let recovered = recover_from_failed_write( error, earlier_step_completed, |state| *guard_told.lock() = Some(state), || { *compensated.lock() = true; async move { if compensation_works { Ok(()) } else { anyhow::bail!("the compensation was refused too") } } }, ) .await; Recovery { guard_told: *guard_told.lock(), compensated: *compensated.lock(), state_unknown: SystemProxyStateUnknown::is_in(&recovered), #[cfg(not(target_os = "linux"))] message: format!("{recovered:#}"), } } fn refused() -> anyhow::Error { anyhow::Error::new(sysproxy::Error::ProxyWrite { progress: sysproxy::WriteProgress::new(0, 7), source: Box::new(sysproxy::Error::RequiresAdminPrivileges), }) } fn partly_written() -> anyhow::Error { anyhow::Error::new(sysproxy::Error::ProxyWrite { progress: sysproxy::WriteProgress::new(3, 7), source: Box::new(sysproxy::Error::RequiresAdminPrivileges), }) } #[tokio::test] async fn a_failure_that_wrote_nothing_is_not_compensated_for() { let recovery = record_recovery(refused(), false, true).await; assert!(!recovery.compensated); assert_eq!(recovery.guard_told, Some(AuthoritativeState::Unchanged)); assert!(!recovery.state_unknown); } #[cfg(target_os = "linux")] #[tokio::test] async fn nothing_on_linux_ever_reaches_the_destructive_row() { for earlier_step_completed in [false, true] { let recovery = record_recovery(partly_written(), earlier_step_completed, true).await; assert!(!recovery.compensated, "{earlier_step_completed}"); assert_eq!(recovery.guard_told, Some(AuthoritativeState::Unchanged)); assert!(!recovery.state_unknown); } } #[cfg(not(target_os = "linux"))] #[tokio::test] async fn a_failure_that_wrote_something_forces_the_proxy_off() { let recovery = record_recovery(partly_written(), false, true).await; assert!(recovery.compensated); assert_eq!(recovery.guard_told, Some(AuthoritativeState::Unknown)); assert!(recovery.state_unknown); } #[cfg(not(target_os = "linux"))] #[tokio::test] async fn a_compensation_that_fails_does_not_replace_the_diagnosis() { let recovery = record_recovery(partly_written(), false, false).await; assert!(recovery.compensated); assert!(recovery.message.contains("admin privileges")); assert!(!recovery.message.contains("the compensation was refused too")); assert!(recovery.state_unknown); } #[cfg(not(target_os = "linux"))] #[tokio::test] async fn an_earlier_setter_that_finished_reaches_the_second_row_too() { let recovery = record_recovery(refused(), true, true).await; assert!(recovery.compensated); assert_eq!(recovery.guard_told, Some(AuthoritativeState::Unknown)); assert!(recovery.state_unknown); } #[tokio::test] async fn a_drained_guard_needs_only_one_write() { assert_eq!( record_disable_sequence(true, true, &[true]).await, (vec!["disable"], true) ); } #[tokio::test] async fn a_write_that_raced_the_guard_is_repeated_after_it_finishes() { assert_eq!( record_disable_sequence(false, true, &[true, true]).await, (vec!["disable", "drain", "disable"], true) ); } #[tokio::test] async fn a_second_write_that_fails_on_its_own_is_still_reported() { assert_eq!( record_disable_sequence(false, true, &[true, false]).await, (vec!["disable", "drain", "disable"], false) ); } #[tokio::test] async fn a_guard_that_never_finishes_still_gets_the_proxy_disabled() { assert_eq!( record_disable_sequence(false, false, &[true, true]).await, (vec!["disable", "drain", "disable"], true) ); } #[tokio::test] async fn a_refused_write_stops_before_waiting_on_anything() { assert_eq!( record_disable_sequence(false, true, &[false]).await, (vec!["disable"], false) ); } #[test] fn a_failure_with_nothing_written_leaves_the_old_guard_worth_restoring() { let refused = anyhow::Error::new(sysproxy::Error::RequiresAdminPrivileges); assert_eq!(authoritative_state(&refused, false), AuthoritativeState::Unchanged); } #[test] fn where_a_successful_setter_proves_nothing_no_failure_is_destructive() { let strongest_evidence = anyhow::Error::new(sysproxy::Error::ProxyWrite { progress: sysproxy::WriteProgress::new(3, 7), source: Box::new(sysproxy::Error::RequiresAdminPrivileges), }); for earlier_step_completed in [false, true] { assert_eq!( authoritative_state_from(false, &strongest_evidence, earlier_step_completed), AuthoritativeState::Unchanged, "{earlier_step_completed}" ); } } fn holding_global(port: u16) -> sysproxy::ProxySnapshot { let endpoint = || sysproxy::ProxyEndpoint { host: "127.0.0.1".to_owned(), port, enable: true, switched_on: true, }; sysproxy::ProxySnapshot { socks: endpoint(), http: endpoint(), https: endpoint(), auto: sysproxy::Autoproxy { url: std::string::String::new(), enable: false, }, auto_switched_on: false, bypass: "localhost".to_owned(), } } fn holding_nothing() -> sysproxy::ProxySnapshot { let off = || sysproxy::ProxyEndpoint { host: std::string::String::new(), port: 0, enable: false, switched_on: false, }; sysproxy::ProxySnapshot { socks: off(), http: off(), https: off(), auto: sysproxy::Autoproxy { url: std::string::String::new(), enable: false, }, auto_switched_on: false, bypass: std::string::String::new(), } } fn global_target(port: u16) -> Sysproxy { Sysproxy { enable: true, host: "127.0.0.1".to_owned(), port, bypass: "localhost".to_owned(), } } fn pac_mode_target() -> Sysproxy { Sysproxy { enable: false, ..global_target(7897) } } fn pac_off() -> Autoproxy { Autoproxy { url: std::string::String::new(), enable: false, } } #[test] fn a_proxy_the_os_already_points_at_needs_no_write() { assert!(target_is_already_in_place( &holding_global(7897), &global_target(7897), &pac_off() )); } #[test] fn a_proxy_pointing_at_a_different_port_still_has_to_be_written() { assert!(!target_is_already_in_place( &holding_global(7897), &global_target(7898), &pac_off() )); } #[test] fn a_pac_target_asks_the_pac_question_and_not_the_global_one() { let pac = Autoproxy { url: "http://127.0.0.1:33333/commands/pac".to_owned(), enable: true, }; let mut os_holds_pac = holding_nothing(); os_holds_pac.auto = pac.clone(); os_holds_pac.auto_switched_on = true; os_holds_pac.bypass = "localhost".to_owned(); assert!(target_is_already_in_place(&os_holds_pac, &pac_mode_target(), &pac)); } #[test] fn a_pac_target_whose_bypass_changed_still_has_to_be_written() { let pac = Autoproxy { url: "http://127.0.0.1:33333/commands/pac".to_owned(), enable: true, }; let mut os_holds_pac = holding_nothing(); os_holds_pac.auto = pac.clone(); os_holds_pac.auto_switched_on = true; os_holds_pac.bypass = "localhost,example.com".to_owned(); assert!(!target_is_already_in_place(&os_holds_pac, &pac_mode_target(), &pac)); } #[test] fn a_switch_left_on_over_nothing_is_not_already_disabled() { let mut stranded = holding_nothing(); stranded.http = sysproxy::ProxyEndpoint { host: std::string::String::new(), port: 0, enable: false, switched_on: true, }; assert!(!target_is_already_in_place( &stranded, &Sysproxy { enable: false, ..global_target(7897) }, &pac_off() )); } #[test] fn a_target_of_nothing_does_not_ask_about_the_bypass_list() { let mut os_holds_nothing = holding_nothing(); os_holds_nothing.bypass = "something.else".to_owned(); assert!(target_is_already_in_place( &os_holds_nothing, &Sysproxy { enable: false, ..global_target(7897) }, &pac_off() )); } #[test] fn a_target_of_nothing_asks_whether_the_os_holds_nothing() { let nothing_wanted = Sysproxy { enable: false, ..global_target(7897) }; assert!(target_is_already_in_place( &holding_nothing(), ¬hing_wanted, &pac_off() )); assert!(!target_is_already_in_place( &holding_global(7897), ¬hing_wanted, &pac_off() )); } #[test] fn only_a_snapshot_that_agrees_with_the_target_skips_the_write() { assert_eq!(classify_os_proxy_state(Ok(true)), OsProxyState::AlreadyApplied); assert_eq!(classify_os_proxy_state(Ok(false)), OsProxyState::DifferentOrUnknown); } #[test] fn a_failed_read_never_counts_as_agreement() { assert_eq!( classify_os_proxy_state(Err(anyhow::anyhow!("read failed"))), OsProxyState::DifferentOrUnknown ); } #[test] fn pure_sysproxy_mode_clears_pac_before_enabling_global_proxy() { assert_eq!( proxy_apply_steps(true, false), [ProxyApplyStep::Autoproxy, ProxyApplyStep::Sysproxy] ); } #[test] fn pac_mode_clears_global_proxy_before_enabling_pac() { assert_eq!( proxy_apply_steps(false, true), [ProxyApplyStep::Sysproxy, ProxyApplyStep::Autoproxy] ); } #[test] fn disabled_mode_clears_global_proxy_before_pac() { assert_eq!( proxy_apply_steps(false, false), [ProxyApplyStep::Sysproxy, ProxyApplyStep::Autoproxy] ); } }