//! macOS SOCKS proxy ownership. Never clear a setting we no longer own. use std::path::Path; #[cfg(target_os = "macos")] mod mac { use super::*; use serde::{Deserialize, Serialize}; use std::io::Write; use std::process::Command; const HOST: &str = "127.0.0.1"; const PORT: &str = "18743"; const RECORD: &str = "system-proxy.json"; #[derive(Serialize, Deserialize)] struct Record { services: Vec } fn networksetup(args: &[&str]) -> Result { let result = Command::new("/usr/sbin/networksetup").args(args).output() .map_err(|e| format!("无法读取系统网络设置:{e}"))?; if !result.status.success() { return Err(format!("无法更新系统代理:{}", String::from_utf8_lossy(&result.stderr).trim())); } Ok(String::from_utf8_lossy(&result.stdout).into_owned()) } fn enabled(value: &str) -> bool { value.lines().any(|line| line.trim().eq_ignore_ascii_case("Enabled: Yes") || line.trim().eq_ignore_ascii_case("Auto Proxy Discovery: On")) } fn services() -> Result, String> { let result = networksetup(&["-listallnetworkservices"])?; Ok(result.lines().skip(1).filter(|s| !s.starts_with('*') && !s.trim().is_empty()) .map(str::to_owned).collect()) } fn ours(service: &str) -> Result { let value = networksetup(&["-getsocksfirewallproxy", service])?; Ok(enabled(&value) && value.lines().any(|v| v.trim() == format!("Server: {HOST}")) && value.lines().any(|v| v.trim() == format!("Port: {PORT}"))) } fn record_path(dir: &Path) -> std::path::PathBuf { dir.join(RECORD) } pub fn active(dir: &Path) -> Result { if !record_path(dir).exists() { return Ok(false); } let record: Record = serde_json::from_slice(&std::fs::read(record_path(dir)).map_err(|e| e.to_string())?) .map_err(|_| "系统代理恢复记录损坏,请检查网络设置".to_string())?; for service in &record.services { if ours(service)? { return Ok(true); } } Ok(false) } pub fn restore(dir: &Path) -> Result<(), String> { let path = record_path(dir); if !path.exists() { return Ok(()); } let record: Record = serde_json::from_slice(&std::fs::read(&path).map_err(|e| e.to_string())?) .map_err(|_| "系统代理恢复记录损坏,请检查网络设置".to_string())?; for service in &record.services { if ours(service)? { networksetup(&["-setsocksfirewallproxystate", service, "off"])?; } } std::fs::remove_file(path).map_err(|e| format!("无法清理系统代理恢复记录:{e}")) } pub fn enable(dir: &Path) -> Result<(), String> { // A stale record may mean a prior process crashed. Never overwrite it. restore(dir)?; let list = services()?; if list.is_empty() { return Err("没有可用的 macOS 网络服务".into()); } for service in &list { for kind in ["-getwebproxy", "-getsecurewebproxy", "-getsocksfirewallproxy", "-getautoproxyurl", "-getproxyautodiscovery"] { let current = networksetup(&[kind, service])?; if enabled(¤t) || (kind == "-getsocksfirewallproxy" && current.lines().any(|line| line.starts_with("Server:") && line.trim() != "Server:" && line.trim() != format!("Server: {HOST}"))) { return Err(format!("网络服务「{service}」已有代理设置,请先关闭其他代理再启用")); } } } std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; // Record ownership before the first OS write, so interrupted startup can recover. let path = record_path(dir); let data = serde_json::to_vec(&Record { services: list.clone() }).map_err(|e| e.to_string())?; let mut file = std::fs::OpenOptions::new().write(true).create_new(true).open(&path) .map_err(|e| format!("无法保存系统代理恢复记录:{e}"))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string())?; } file.write_all(&data).and_then(|_| file.sync_all()).map_err(|e| format!("无法持久化系统代理恢复记录:{e}"))?; for service in &list { if let Err(err) = networksetup(&["-setsocksfirewallproxy", service, HOST, PORT]) { return match restore(dir) { Ok(()) => Err(err), Err(recovery) => Err(format!("{err};恢复失败:{recovery}")) }; } } Ok(()) } #[cfg(test)] mod tests { use super::enabled; #[test] fn parses_networksetup_state() { assert!(enabled("Enabled: Yes\nServer: 127.0.0.1")); assert!(!enabled("Enabled: No\nServer: 127.0.0.1")); } } } #[cfg(target_os = "macos")] pub use mac::{active, enable, restore}; #[cfg(not(target_os = "macos"))] pub fn active(_: &Path) -> Result { Ok(false) } #[cfg(not(target_os = "macos"))] pub fn restore(_: &Path) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "macos"))] pub fn enable(_: &Path) -> Result<(), String> { Err("系统代理目前仅支持 macOS".into()) }