mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-02 04:42:09 +08:00
fix(sysproxy): stop turning a missing network service into a hard failure
This commit is contained in:
@@ -162,6 +162,24 @@ fn was_refused_locally(error: &anyhow::Error) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether macOS has no network service to write the proxy on.
|
||||
///
|
||||
/// Offline, mid network switch, or the primary interface is not a configured service. A read
|
||||
/// reports "no proxy" and a disable has nothing left to do; an enable still fails, since a proxy
|
||||
/// the OS never received must not be recorded as applied.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const fn is_missing_network_service(error: &sysproxy::Error) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
sysproxy::Error::NoActiveNetworkService | sysproxy::Error::NetworkInterface
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const fn is_missing_network_service(_error: &sysproxy::Error) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Distinguish a misplaced sidecar from a missing privileged service.
|
||||
async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error {
|
||||
if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) {
|
||||
@@ -340,6 +358,10 @@ fn service_proxy_config(verge: &IVerge, mixed_port: u16, pac_port: u16) -> Resul
|
||||
}
|
||||
}
|
||||
|
||||
/// Judge the outcome the service reported.
|
||||
///
|
||||
/// A write it could not roll back arrives as a plain error whose cause is the rollback's, not
|
||||
/// the write's, so it stays a failure.
|
||||
fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome) -> Result<()> {
|
||||
match outcome {
|
||||
ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()),
|
||||
@@ -389,16 +411,21 @@ where
|
||||
Write: FnOnce() -> WriteFuture,
|
||||
WriteFuture: Future<Output = Result<()>>,
|
||||
{
|
||||
ensure!(
|
||||
drain_local_guard().await,
|
||||
"the system proxy guard did not stop in time; not writing the system proxy through the service"
|
||||
);
|
||||
if !drain_local_guard().await {
|
||||
// Its `networksetup` calls are separate from the service's one privileged transaction,
|
||||
// so a stale guard write may still land after ours.
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"the system proxy guard did not stop in time; writing through the service anyway"
|
||||
);
|
||||
}
|
||||
write().await
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TableEffect<'a> {
|
||||
Retire { enabled: bool },
|
||||
Retire(FailedOperation),
|
||||
File(FailedOperation, &'a anyhow::Error),
|
||||
Nothing,
|
||||
}
|
||||
@@ -415,19 +442,19 @@ fn clear_table_effect(result: &Result<()>) -> TableEffect<'_> {
|
||||
|
||||
fn settle_table<Retire, File>(effect: TableEffect<'_>, retire: Retire, file: File)
|
||||
where
|
||||
Retire: FnOnce(bool),
|
||||
Retire: FnOnce(FailedOperation),
|
||||
File: FnOnce(FailedOperation, &anyhow::Error),
|
||||
{
|
||||
match effect {
|
||||
TableEffect::Retire { enabled } => retire(enabled),
|
||||
TableEffect::Retire(asked) => retire(asked),
|
||||
TableEffect::File(operation, error) => file(operation, error),
|
||||
TableEffect::Nothing => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn table_effect(result: &Result<()>, enabled: bool) -> TableEffect<'_> {
|
||||
fn table_effect(result: &Result<()>) -> TableEffect<'_> {
|
||||
match result {
|
||||
Ok(()) => TableEffect::Retire { enabled },
|
||||
Ok(()) => TableEffect::Retire(notification::what_was_asked()),
|
||||
Err(error) if SysproxyFailure::from_chain(error).is_some() => {
|
||||
TableEffect::File(notification::what_was_asked(), error)
|
||||
}
|
||||
@@ -437,9 +464,7 @@ fn table_effect(result: &Result<()>, enabled: bool) -> TableEffect<'_> {
|
||||
|
||||
pub async fn apply() -> Result<()> {
|
||||
let running_mode = CoreManager::global().get_running_mode();
|
||||
// Use the pre-apply snapshot when retiring matching failures.
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let enabled = verge.enable_system_proxy.unwrap_or_default();
|
||||
let result = match proxy_backend_route(cfg!(target_os = "macos"), &running_mode) {
|
||||
ProxyBackendRoute::Local => match Sysopt::global().update_sysproxy().await {
|
||||
Ok(()) => Ok(()),
|
||||
@@ -462,7 +487,7 @@ pub async fn apply() -> Result<()> {
|
||||
};
|
||||
// Settle failures where every proxy-apply path converges.
|
||||
settle_table(
|
||||
table_effect(&result, enabled),
|
||||
table_effect(&result),
|
||||
notification::retire_system_proxy_failures,
|
||||
|operation, error| {
|
||||
report_failure(operation, error);
|
||||
@@ -906,8 +931,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_local_guard_that_would_not_stop_keeps_root_from_writing() {
|
||||
assert_eq!(record_service_apply(false, true).await, (vec!["drain"], false));
|
||||
async fn a_local_guard_that_would_not_stop_does_not_block_the_service_write() {
|
||||
assert_eq!(record_service_apply(false, true).await, (vec!["drain", "write"], true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1044,7 +1069,45 @@ mod tests {
|
||||
assert_eq!(refusal_classification(None), SysproxyFailure::PrivilegeRequired);
|
||||
}
|
||||
|
||||
use super::{SystemProxyStateUnknown, is_reportable_given, rollback_failure};
|
||||
use super::{SystemProxyStateUnknown, is_reportable_given, rollback_failure, service_apply_result};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn an_enable_the_os_never_received_is_never_reported_as_applied() {
|
||||
let requested = MacosProxyConfig::Global {
|
||||
host: "127.0.0.1".to_owned(),
|
||||
port: 7890,
|
||||
bypass: String::new(),
|
||||
};
|
||||
|
||||
let error = service_apply_result(
|
||||
&requested,
|
||||
ProxyApplyOutcome::DirectFallback {
|
||||
message: sysproxy::Error::NoActiveNetworkService.to_string(),
|
||||
},
|
||||
)
|
||||
.err()
|
||||
.unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail"));
|
||||
|
||||
assert_eq!(
|
||||
SysproxyFailure::from_chain(&error).map(SysproxyFailure::code),
|
||||
Some("SYSPROXY_DIRECT_FALLBACK")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn a_disable_is_satisfied_by_the_os_ending_up_direct() {
|
||||
assert!(
|
||||
service_apply_result(
|
||||
&MacosProxyConfig::Disabled,
|
||||
ProxyApplyOutcome::DirectFallback {
|
||||
message: sysproxy::Error::NoActiveNetworkService.to_string(),
|
||||
},
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
fn mappable() -> anyhow::Error {
|
||||
anyhow::anyhow!("networksetup refused").context(SysproxyFailure::PrivilegeRequired)
|
||||
@@ -1054,7 +1117,7 @@ mod tests {
|
||||
let events = Mutex::new(Vec::new());
|
||||
super::settle_table(
|
||||
effect,
|
||||
|enabled| events.lock().push(format!("retire:{enabled}")),
|
||||
|asked| events.lock().push(format!("retire:{asked:?}")),
|
||||
|operation, _| events.lock().push(format!("file:{operation:?}")),
|
||||
);
|
||||
events.into_inner()
|
||||
@@ -1063,8 +1126,8 @@ mod tests {
|
||||
#[test]
|
||||
fn each_effect_reaches_the_table_it_names() {
|
||||
assert_eq!(
|
||||
record_settlement(super::TableEffect::Retire { enabled: true }),
|
||||
vec!["retire:true"]
|
||||
record_settlement(super::TableEffect::Retire(FailedOperation::SystemProxyEnable)),
|
||||
vec!["retire:SystemProxyEnable"]
|
||||
);
|
||||
assert_eq!(
|
||||
record_settlement(super::TableEffect::File(
|
||||
@@ -1092,16 +1155,20 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clean_apply_retires_in_the_direction_it_reached() {
|
||||
#[tokio::test]
|
||||
async fn a_clean_apply_retires_in_the_direction_that_was_asked_for() {
|
||||
assert!(matches!(
|
||||
super::table_effect(&Ok(()), true),
|
||||
super::TableEffect::Retire { enabled: true }
|
||||
));
|
||||
assert!(matches!(
|
||||
super::table_effect(&Ok(()), false),
|
||||
super::TableEffect::Retire { enabled: false }
|
||||
super::table_effect(&Ok(())),
|
||||
super::TableEffect::Retire(FailedOperation::SystemProxyRestore)
|
||||
));
|
||||
let asked = super::notification::asking_for(FailedOperation::SystemProxyEnable, async {
|
||||
matches!(
|
||||
super::table_effect(&Ok(())),
|
||||
super::TableEffect::Retire(FailedOperation::SystemProxyEnable)
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert!(asked);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1109,7 +1176,7 @@ mod tests {
|
||||
let (applied, cleared) = super::notification::asking_for(FailedOperation::SystemProxyEnable, async {
|
||||
let (apply_error, clear_error) = (mappable(), mappable());
|
||||
(
|
||||
format!("{:?}", super::table_effect(&Err(apply_error), true)),
|
||||
format!("{:?}", super::table_effect(&Err(apply_error))),
|
||||
format!("{:?}", super::clear_table_effect(&Err(clear_error))),
|
||||
)
|
||||
})
|
||||
@@ -1129,7 +1196,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_failure_with_nothing_to_say_is_not_filed() {
|
||||
assert!(matches!(
|
||||
super::table_effect(&Err(anyhow::anyhow!("the core exited immediately")), true),
|
||||
super::table_effect(&Err(anyhow::anyhow!("the core exited immediately"))),
|
||||
super::TableEffect::Nothing
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user