feat: linux auto-sudo and tauri system tray background mode

This commit is contained in:
ospab 2026-06-02 22:58:04 +03:00
parent 5e4fd2be02
commit fb0dbf9da1
3 changed files with 141 additions and 0 deletions

View File

@ -111,6 +111,41 @@ fn relaunch_as_admin() -> Result<()> {
std::process::exit(0); std::process::exit(0);
} }
#[cfg(target_os = "linux")]
pub fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
#[cfg(target_os = "linux")]
fn relaunch_as_root() -> Result<()> {
use std::io::IsTerminal;
let exe = std::env::current_exe()?;
let args: Vec<String> = std::env::args().skip(1).collect();
let is_gui = std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok();
let is_term = std::io::stdout().is_terminal();
let mut cmd = if is_gui && !is_term {
let mut c = std::process::Command::new("pkexec");
c.arg(exe);
c
} else {
let mut c = std::process::Command::new("sudo");
c.arg(exe);
c
};
cmd.args(&args);
let status = cmd.status().map_err(|e| anyhow::anyhow!("Failed to execute privilege escalation command: {}", e))?;
if !status.success() {
return Err(anyhow::anyhow!("Privilege escalation failed or was denied."));
}
std::process::exit(0);
}
pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> { pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
if config.mode == "tun" && !is_admin() { if config.mode == "tun" && !is_admin() {
@ -118,6 +153,12 @@ pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> {
relaunch_as_admin()?; relaunch_as_admin()?;
} }
#[cfg(target_os = "linux")]
if config.mode == "tun" && !is_root() {
println!("[ostp] TUN mode requires root privileges. Requesting sudo/pkexec elevation...");
relaunch_as_root()?;
}
let bg = std::env::args().any(|a| a == "--bg"); let bg = std::env::args().any(|a| a == "--bg");
if bg { if bg {
@ -152,6 +193,11 @@ pub async fn run_client_core(
return Err(anyhow::anyhow!("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator.")); return Err(anyhow::anyhow!("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator."));
} }
#[cfg(target_os = "linux")]
if config.mode == "tun" && !is_root() {
return Err(anyhow::anyhow!("Root privileges are required to initialize TUN mode on Linux. Please run with sudo."));
}
log_to_core_file(&format!("[core] Starting run_client_core in mode: {}", config.mode)); log_to_core_file(&format!("[core] Starting run_client_core in mode: {}", config.mode));
// Resolve the server IP before we override system routing and DNS. // Resolve the server IP before we override system routing and DNS.

View File

@ -522,6 +522,91 @@ pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.manage(state) .manage(state)
.setup(|app| {
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{TrayIconBuilder, TrayIconEvent, MouseButton, MouseButtonState};
use tauri::Manager;
let config_path = get_config_path();
let mut masked_ip = String::from("0.0.0.0");
if config_path.exists() {
if let Ok(content) = std::fs::read_to_string(&config_path) {
let mut stripped = json_comments::StripComments::new(content.as_bytes());
if let Ok(val) = serde_json::from_reader::<_, serde_json::Value>(&mut stripped) {
if let Some(server) = val.get("server").and_then(|s| s.as_str()) {
let parts: Vec<&str> = server.split(':').collect();
let ip = parts[0];
let port = if parts.len() > 1 { parts[1] } else { "" };
let octets: Vec<&str> = ip.split('.').collect();
if octets.len() == 4 {
masked_ip = format!("{}.{}.**.**:{}", octets[0], octets[1], port);
} else if octets.len() > 2 {
masked_ip = format!("{}...:{}", octets[0], port);
} else {
masked_ip = server.to_string();
}
}
}
}
}
let connect_i = MenuItem::with_id(app, "connect", "Подключиться", true, None::<&str>)?;
let disconnect_i = MenuItem::with_id(app, "disconnect", "Отключиться", true, None::<&str>)?;
let server_i = MenuItem::with_id(app, "server", format!("Сервер: {}", masked_ip), false, None::<&str>)?;
let show_i = MenuItem::with_id(app, "show", "Показать окно", true, None::<&str>)?;
let exit_i = MenuItem::with_id(app, "exit", "Выход", true, None::<&str>)?;
let menu = Menu::with_items(app, &[
&server_i,
&connect_i,
&disconnect_i,
&show_i,
&exit_i,
])?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| {
match event.id.as_ref() {
"connect" => {
let _ = app.emit("tray_connect", ());
}
"disconnect" => {
let _ = app.emit("tray_disconnect", ());
}
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"exit" => {
app.exit(0);
}
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
})
.on_window_event(|window, event| match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
let _ = window.hide();
api.prevent_close();
}
_ => {}
})
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, get_tunnel_status, get_metrics, get_config, save_config]) .invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, get_tunnel_status, get_metrics, get_config, save_config])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@ -513,4 +513,14 @@ window.addEventListener('DOMContentLoaded', async () => {
const code = await invoke('get_tunnel_status'); const code = await invoke('get_tunnel_status');
if (code > 0) startPolling(); if (code > 0) startPolling();
} catch { /* not in Tauri context */ } } catch { /* not in Tauri context */ }
if (window.__TAURI__?.event) {
const { listen } = window.__TAURI__.event;
listen('tray_connect', () => {
if (appState === 'disconnected') handleToggle();
});
listen('tray_disconnect', () => {
if (appState !== 'disconnected') handleToggle();
});
}
}); });