src-tauri/src/elevation.rs
Rama main · réplica de lectura del repositorio original.
//! Права администратора (нужны для TUN-режима на Windows).
use tauri::AppHandle;
#[cfg(windows)]
mod imp {
use super::*;
/// Запущено ли приложение с правами администратора.
pub fn is_elevated() -> bool {
use std::mem;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Security::{
GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
unsafe {
let mut token = std::ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
return false;
}
let mut elevation = TOKEN_ELEVATION {
TokenIsElevated: 0,
};
let mut size = 0u32;
let ok = GetTokenInformation(
token,
TokenElevation,
&mut elevation as *mut _ as *mut _,
mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut size,
);
CloseHandle(token);
ok != 0 && elevation.TokenIsElevated != 0
}
}
/// Перезапускает приложение с правами администратора (UAC) и закрывает текущий процесс.
pub fn relaunch_elevated(app: &AppHandle) -> Result<(), String> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::UI::Shell::ShellExecuteW;
use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let to_wide = |s: &std::ffi::OsStr| -> Vec<u16> {
s.encode_wide().chain(std::iter::once(0)).collect()
};
let exe_w = to_wide(exe.as_os_str());
let verb_w = to_wide(std::ffi::OsStr::new("runas"));
let h = unsafe {
ShellExecuteW(
std::ptr::null_mut(),
verb_w.as_ptr(),
exe_w.as_ptr(),
std::ptr::null(),
std::ptr::null(),
SW_SHOWNORMAL,
)
};
// ShellExecuteW: значение <= 32 — ошибка (в т.ч. отказ UAC)
if (h as isize) <= 32 {
return Err("не удалось перезапустить с правами администратора (отклонено?)".into());
}
app.exit(0);
Ok(())
}
}
#[cfg(unix)]
mod imp {
use super::*;
/// Запущено ли приложение от root (euid == 0). На macOS TUN-режим требует root.
pub fn is_elevated() -> bool {
// SAFETY: geteuid не имеет side-effects и всегда успешен.
unsafe { libc::geteuid() == 0 }
}
/// Повышение прав на macOS: единого «перезапуска с UAC», как на Windows, нет.
/// Root нужен только процессу sing-box в TUN-режиме — его поднимаем через
/// `osascript ... with administrator privileges` в connection.rs (не весь GUI).
/// Здесь возвращаем понятную ошибку, если код всё же сюда попал.
pub fn relaunch_elevated(_app: &AppHandle) -> Result<(), String> {
Err("на macOS права запрашиваются при подключении TUN, а не перезапуском приложения".into())
}
}
pub use imp::{is_elevated, relaunch_elevated};
/// Выполняет shell-команду от root через osascript (нативный macOS-диалог
/// пароля администратора). Возвращает вывод команды при успехе.
#[cfg(target_os = "macos")]
pub fn osascript_admin(shell: &str) -> Result<std::process::Output, String> {
// экранирование для строкового литерала AppleScript ("..." и \)
let apple = format!(
"do shell script \"{}\" with administrator privileges",
shell.replace('\\', "\\\\").replace('"', "\\\"")
);
let out = std::process::Command::new("osascript")
.arg("-e")
.arg(&apple)
.output()
.map_err(|e| format!("не удалось запустить osascript: {e}"))?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
// -128 / "User canceled" — пользователь отклонил запрос пароля
if err.contains("-128") || err.to_lowercase().contains("cancel") {
return Err("запрос прав администратора отклонён".into());
}
return Err(format!("osascript: {}", err.trim()));
}
Ok(out)
}
// ---------------------------------------------------------------------------
// Windows: запуск с правами администратора БЕЗ UAC-запроса — через задачу
// Планировщика с RunLevel=HighestAvailable. Задача создаётся один раз из
// elevated-процесса (одно подтверждение UAC), дальше любой запуск (автозапуск,
// двойной клик) идёт редиректом `schtasks /Run` → элевация без запроса.
// ---------------------------------------------------------------------------
#[cfg(windows)]
mod admin_task {
use std::os::windows::process::CommandExt;
const TASK_NAME: &str = "UniGate";
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
fn schtasks(args: &[&str]) -> Result<std::process::Output, String> {
std::process::Command::new("schtasks")
.args(args)
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("schtasks: {e}"))
}
pub fn exists() -> bool {
schtasks(&["/Query", "/TN", TASK_NAME])
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Создаёт/пересоздаёт задачу под текущий путь exe (идемпотентно, лечит
/// переезд приложения). Требует elevated-процесса. XML вместо флагов
/// `/SC ONCE /ST` — без триггеров и без локале-зависимых дат; важно
/// `ExecutionTimeLimit=PT0S` (иначе планировщик убьёт приложение через 72 ч)
/// и `MultipleInstancesPolicy=Parallel` (иначе /Run откажет при живом инстансе).
pub fn install() -> Result<(), String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let exe_dir = exe.parent().map(|p| p.to_path_buf()).unwrap_or_default();
let esc = |s: &str| s.replace('&', "&").replace('<', "<").replace('>', ">");
let xml = format!(
r#"<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>Запуск UniGate с правами администратора без UAC</Description>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>4</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>{cmd}</Command>
<WorkingDirectory>{wd}</WorkingDirectory>
</Exec>
</Actions>
</Task>
"#,
cmd = esc(&exe.to_string_lossy()),
wd = esc(&exe_dir.to_string_lossy()),
);
// schtasks ждёт файл в UTF-16LE с BOM
let mut bytes: Vec<u8> = vec![0xFF, 0xFE];
for u in xml.encode_utf16() {
bytes.extend_from_slice(&u.to_le_bytes());
}
let path = std::env::temp_dir().join("unigate-task.xml");
std::fs::write(&path, &bytes).map_err(|e| format!("запись XML задачи: {e}"))?;
let out = schtasks(&["/Create", "/TN", TASK_NAME, "/XML", &path.to_string_lossy(), "/F"])?;
let _ = std::fs::remove_file(&path);
if !out.status.success() {
return Err(format!(
"schtasks /Create: {}",
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(())
}
pub fn delete() -> Result<(), String> {
let out = schtasks(&["/Delete", "/TN", TASK_NAME, "/F"])?;
if !out.status.success() {
return Err(format!(
"schtasks /Delete: {}",
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(())
}
pub fn run() -> Result<(), String> {
let out = schtasks(&["/Run", "/TN", TASK_NAME])?;
if !out.status.success() {
return Err(format!(
"schtasks /Run: {}",
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(())
}
}
/// Обслуживание «запуска от администратора» на старте приложения (Windows).
/// - включено + elevated: пересоздаём задачу под текущий путь (самолечение);
/// - включено + НЕ elevated + задача есть: перезапускаемся через неё (без UAC)
/// и завершаем текущий процесс;
/// - выключено + elevated: подчищаем задачу, если осталась.
/// Возвращает true, если текущий процесс нужно завершить (произошёл редирект).
#[cfg(windows)]
pub fn admin_launch_startup(app: &AppHandle) -> bool {
let Ok(s) = crate::settings::load(app) else {
return false;
};
if !s.admin_launch {
if is_elevated() && admin_task::exists() {
let _ = admin_task::delete();
}
return false;
}
if is_elevated() {
if let Err(e) = admin_task::install() {
eprintln!("не удалось обновить задачу планировщика: {e}");
}
return false;
}
if admin_task::exists() {
// задача запускает exe без аргументов — оставляем маркер, чтобы
// elevated-инстанс знал, что запуск пришёл из автозапуска (старт в трее)
let via_autostart = std::env::args().any(|a| a == crate::AUTOSTART_ARG);
let marker = crate::storage::path(app, crate::AUTOSTART_MARKER).ok();
if via_autostart {
if let Some(p) = &marker {
let _ = std::fs::write(p, b"1");
}
}
if admin_task::run().is_ok() {
return true; // elevated-инстанс запущен задачей — этот процесс закрываем
}
if via_autostart {
if let Some(p) = &marker {
let _ = std::fs::remove_file(p);
}
}
}
// задачи нет (удалили руками) — молча работаем без прав: UAC при автозапуске
// показывать нельзя, а TUN при подключении сам предложит перезапуск
false
}
/// Применяет настройку «запуск от администратора» (вызывается из UI).
/// Windows: включение из обычного процесса → один UAC-перезапуск, задачу
/// создаст elevated-инстанс на старте; из elevated — задача создаётся сразу.
/// Выключение — удаляем задачу (из-под обычного пользователя может не выйти —
/// тогда её подчистит следующий elevated-запуск).
#[tauri::command]
pub fn apply_admin_launch(app: AppHandle) -> Result<(), String> {
#[cfg(windows)]
{
let s = crate::settings::load(&app)?;
if s.admin_launch {
if is_elevated() {
admin_task::install()
} else {
relaunch_elevated(&app)
}
} else {
if admin_task::exists() {
let _ = admin_task::delete();
}
Ok(())
}
}
#[cfg(not(windows))]
{
let _ = app;
Err("запуск от администратора без запроса доступен только на Windows".into())
}
}
#[tauri::command]
pub fn is_elevated_cmd() -> bool {
is_elevated()
}
#[tauri::command]
pub fn relaunch_elevated_cmd(app: AppHandle) -> Result<(), String> {
relaunch_elevated(&app)
}