Files
opsx-vpn/client/src-tauri/src/system_proxy.rs
T

122 lines
5.4 KiB
Rust

//! 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<String> }
fn networksetup(args: &[&str]) -> Result<String, String> {
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<Vec<String>, 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<bool, String> {
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<bool, String> {
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(&current) || (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<bool, String> { 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()) }