//! Управление системным HTTP-прокси. //! //! Phase 3: Windows — через реестр (`Internet Settings`) + уведомление WinINet, //! чтобы изменение подхватилось без перезапуска приложений. Прав администратора //! не требует (пишем в HKCU). //! Phase 10: macOS — через `networksetup` (web/secure/socks-прокси на активном //! сетевом сервисе). Тоже без прав администратора. Прочие ОС (Linux) — заглушки. #[cfg(windows)] mod imp { use winreg::enums::*; use winreg::RegKey; const INTERNET_SETTINGS: &str = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings"; fn open() -> Result { let hkcu = RegKey::predef(HKEY_CURRENT_USER); hkcu.open_subkey_with_flags(INTERNET_SETTINGS, KEY_READ | KEY_WRITE) .map_err(|e| format!("не удалось открыть Internet Settings: {e}")) } /// Уведомляет WinINet, что настройки прокси изменились (немедленный эффект). fn notify_changed() { use windows_sys::Win32::Networking::WinInet::{ InternetSetOptionW, INTERNET_OPTION_REFRESH, INTERNET_OPTION_SETTINGS_CHANGED, }; unsafe { InternetSetOptionW(std::ptr::null_mut(), INTERNET_OPTION_SETTINGS_CHANGED, std::ptr::null_mut(), 0); InternetSetOptionW(std::ptr::null_mut(), INTERNET_OPTION_REFRESH, std::ptr::null_mut(), 0); } } pub fn enable(port: u16) -> Result<(), String> { let key = open()?; key.set_value("ProxyServer", &format!("127.0.0.1:{port}")) .map_err(|e| format!("ProxyServer: {e}"))?; // локальные адреса — в обход прокси key.set_value("ProxyOverride", &"localhost;127.*;10.*;172.16.*;192.168.*;") .map_err(|e| format!("ProxyOverride: {e}"))?; key.set_value("ProxyEnable", &1u32) .map_err(|e| format!("ProxyEnable: {e}"))?; notify_changed(); Ok(()) } pub fn disable() -> Result<(), String> { let key = open()?; key.set_value("ProxyEnable", &0u32) .map_err(|e| format!("ProxyEnable: {e}"))?; notify_changed(); Ok(()) } /// true, если системный прокси включён и указывает именно на наш порт. pub fn is_enabled_for(port: u16) -> bool { let Ok(key) = open() else { return false }; let enabled: u32 = key.get_value("ProxyEnable").unwrap_or(0); let server: String = key.get_value("ProxyServer").unwrap_or_default(); enabled == 1 && server == format!("127.0.0.1:{port}") } } #[cfg(target_os = "macos")] mod imp { //! Системный прокси на macOS через `networksetup`. Ставим web (HTTP), //! secure web (HTTPS) и SOCKS прокси на 127.0.0.1: — sing-box //! `mixed`-inbound слушает и HTTP, и SOCKS на одном порту. use std::process::Command; /// Захват stdout команды `networksetup `. fn output(args: &[&str]) -> Option { let out = Command::new("networksetup").args(args).output().ok()?; Some(String::from_utf8_lossy(&out.stdout).into_owned()) } /// Включённые сетевые сервисы с реальным аппаратным интерфейсом (en0/en9/…). /// Отключённые (`*`) и VPN-псевдосервисы (пустой `Device`) пропускаем: прокси /// на loopback (127.0.0.1) имеет смысл ставить только на физические сервисы, /// и работает он независимо от того, какой интерфейс сейчас аплинк (в т.ч. если /// маршрут по умолчанию уже уводит другой VPN-туннель `utunN`). /// /// Формат `networksetup -listnetworkserviceorder`: /// ```text /// (1) Wi-Fi /// (Hardware Port: Wi-Fi, Device: en0) /// ``` /// Отключённый сервис — `(*) Name`. fn hardware_services() -> Vec { let Some(order) = output(&["-listnetworkserviceorder"]) else { return Vec::new(); }; let mut services = Vec::new(); let mut pending: Option = None; // имя сервиса, ждём строку деталей for line in order.lines() { let l = line.trim(); if l.starts_with("(Hardware Port:") { // деталь текущего сервиса — оставляем сервис, только если есть Device let device = l .split("Device:") .nth(1) .map(|d| d.trim().trim_end_matches(')').trim()) .unwrap_or(""); if !device.is_empty() { if let Some(name) = pending.take() { services.push(name); } } else { pending = None; } } else if let Some(rest) = l.strip_prefix('(') { // строка сервиса "(N) Name" / "(*) Name" if let Some(idx) = rest.find(") ") { let marker = &rest[..idx]; let name = rest[idx + 2..].trim().to_string(); pending = if marker.contains('*') { None } else { Some(name) }; } } } services } /// Обход прокси для локальных адресов (аналог `ProxyOverride` на Windows): /// localhost/LAN/mDNS — напрямую, иначе ломаются локальные dev-серверы, /// принтеры, доступ к роутеру и т.п. const BYPASS: &[&str] = &[ "localhost", "127.0.0.1", "*.local", "169.254/16", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", ]; /// Полный набор команд networksetup для включения прокси на сервисах. fn enable_cmds(services: &[String], port: u16) -> Vec> { let p = port.to_string(); let mut cmds = Vec::new(); for s in services { for set in ["-setwebproxy", "-setsecurewebproxy", "-setsocksfirewallproxy"] { cmds.push(vec![set.into(), s.clone(), "127.0.0.1".into(), p.clone()]); } let mut b = vec!["-setproxybypassdomains".to_string(), s.clone()]; b.extend(BYPASS.iter().map(|x| x.to_string())); cmds.push(b); } cmds } /// Набор команд для выключения прокси на сервисах. fn disable_cmds(services: &[String]) -> Vec> { let mut cmds = Vec::new(); for s in services { for set in [ "-setwebproxystate", "-setsecurewebproxystate", "-setsocksfirewallproxystate", ] { cmds.push(vec![set.into(), s.clone(), "off".into()]); } } cmds } /// Запуск команды с аргументами-строками (как `run`, но для Vec). fn run_args(args: &[String]) -> Result<(), String> { let status = Command::new("networksetup") .args(args) .status() .map_err(|e| format!("networksetup: {e}"))?; if !status.success() { return Err(format!( "networksetup {:?} завершился с кодом {:?}", args, status.code() )); } Ok(()) } /// Фолбэк: весь батч команд одним `osascript ... with administrator /// privileges`. У админ-пользователей networksetup обычно работает и так, /// но на части систем set-команды требуют прав (exit 14) — тогда один /// нативный запрос пароля вместо запуска всего приложения от root. fn admin_batch(cmds: &[Vec]) -> Result<(), String> { let q = |s: &str| s.replace('\'', "'\\''"); let shell = cmds .iter() .map(|c| { let args = c .iter() .map(|a| format!("'{}'", q(a))) .collect::>() .join(" "); format!("/usr/sbin/networksetup {args}") }) .collect::>() .join("; "); crate::elevation::osascript_admin(&shell).map(|_| ()) } pub fn enable(port: u16) -> Result<(), String> { let services = hardware_services(); if services.is_empty() { return Err("не найдено сетевых сервисов для установки прокси".into()); } // Ставим на все физические сервисы (best-effort): какой из них активен — // тот и подхватит; set-команды одновременно включают прокси. let mut ok_any = false; let mut failed: Vec> = Vec::new(); for service in &services { let cmds = enable_cmds(std::slice::from_ref(service), port); let mut service_ok = true; for c in &cmds { if let Err(e) = run_args(c) { eprintln!("[sysproxy] {service}: {e}"); service_ok = false; } } if service_ok { ok_any = true; } else { failed.extend(cmds); } } if ok_any { return Ok(()); } // ни один сервис не настроился без прав — пробуем один раз от админа admin_batch(&failed) .map_err(|e| format!("не удалось включить системный прокси: {e}")) } pub fn disable() -> Result<(), String> { // снимаем на всех физических сервисах (best-effort) let services = hardware_services(); let cmds = disable_cmds(&services); let mut all_ok = true; for c in &cmds { if run_args(c).is_err() { all_ok = false; } } if all_ok || !any_proxy_enabled(&services) { return Ok(()); } // без прав снять не вышло, а прокси реально включён — просим пароль, // иначе у пользователя останется «мёртвый» прокси без интернета admin_batch(&cmds) } /// true, если на каком-то сервисе включён хотя бы один из наших прокси. fn any_proxy_enabled(services: &[String]) -> bool { for service in services { for get in ["-getwebproxy", "-getsecurewebproxy", "-getsocksfirewallproxy"] { if let Some(info) = output(&[get, service]) { if info.lines().any(|l| l.trim() == "Enabled: Yes") { return true; } } } } false } /// true, если на каком-либо сервисе web-прокси включён и указывает на наш порт. pub fn is_enabled_for(port: u16) -> bool { let want_port = format!("Port: {port}"); for service in hardware_services() { let Some(info) = output(&["-getwebproxy", &service]) else { continue; }; let enabled = info.lines().any(|l| l.trim() == "Enabled: Yes"); let host_ok = info.lines().any(|l| l.trim() == "Server: 127.0.0.1"); let port_ok = info.lines().any(|l| l.trim() == want_port); if enabled && host_ok && port_ok { return true; } } false } } #[cfg(not(any(windows, target_os = "macos")))] mod imp { pub fn enable(_port: u16) -> Result<(), String> { Ok(()) // TODO Linux: системный прокси (gsettings/env) — позже } pub fn disable() -> Result<(), String> { Ok(()) } pub fn is_enabled_for(_port: u16) -> bool { false } } pub use imp::{disable, enable, is_enabled_for};