From fb0dbf9da1e4433bac9592c3605bc38cc1809221 Mon Sep 17 00:00:00 2001 From: ospab Date: Tue, 2 Jun 2026 22:58:04 +0300 Subject: [PATCH] feat: linux auto-sudo and tauri system tray background mode --- ostp-client/src/runner.rs | 46 +++++++++++++++++++ ostp-gui/src-tauri/src/lib.rs | 85 +++++++++++++++++++++++++++++++++++ ostp-gui/src/main.js | 10 +++++ 3 files changed, 141 insertions(+) diff --git a/ostp-client/src/runner.rs b/ostp-client/src/runner.rs index b1522f6..dfd8650 100644 --- a/ostp-client/src/runner.rs +++ b/ostp-client/src/runner.rs @@ -111,6 +111,41 @@ fn relaunch_as_admin() -> Result<()> { 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 = 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<()> { #[cfg(target_os = "windows")] if config.mode == "tun" && !is_admin() { @@ -118,6 +153,12 @@ pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> { 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"); 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.")); } + #[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)); // Resolve the server IP before we override system routing and DNS. diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 85b17c5..aaacbe2 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -522,6 +522,91 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .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]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index 0bdcfcc..70c409c 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -513,4 +513,14 @@ window.addEventListener('DOMContentLoaded', async () => { const code = await invoke('get_tunnel_status'); if (code > 0) startPolling(); } 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(); + }); + } });