use std::{ path::{Path, PathBuf}, process::Command, }; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; #[cfg(target_os = "windows")] const CREATE_NO_WINDOW: u32 = 0x08000000; pub fn current_tunnel_strategy() -> &'static str { if cfg!(target_os = "windows") { "embedded-wireguard-windows-x64" } else if cfg!(target_os = "macos") { "embedded-wireguard-macos-arm" } else { "unsupported" } } pub fn connect(app: &AppHandle, profile_path: &Path) -> Result<(), String> { let backend = bundled_backend(app)?; let mut command = Command::new(backend); command.arg("connect").arg("--profile").arg(profile_path); #[cfg(target_os = "windows")] command.creation_flags(CREATE_NO_WINDOW); let output = command .output() .map_err(|err| format!("Unable to start embedded tunnel backend: {}", err))?; if !output.status.success() { return Err(format_helper_error("connect", &output)); } Ok(()) } pub fn disconnect(app: &AppHandle, profile_path: &Path) -> Result<(), String> { let backend = bundled_backend(app)?; let mut command = Command::new(backend); command.arg("disconnect").arg("--profile").arg(profile_path); #[cfg(target_os = "windows")] command.creation_flags(CREATE_NO_WINDOW); let output = command .output() .map_err(|err| format!("Unable to stop embedded tunnel backend: {}", err))?; if !output.status.success() { return Err(format_helper_error("disconnect", &output)); } Ok(()) } pub fn is_active(app: &AppHandle, profile_path: &Path) -> Result { let backend = bundled_backend(app)?; let mut command = Command::new(backend); command.arg("status").arg("--profile").arg(profile_path); #[cfg(target_os = "windows")] command.creation_flags(CREATE_NO_WINDOW); let output = command .output() .map_err(|err| format!("Unable to query embedded tunnel backend status: {}", err))?; if !output.status.success() { return Err(format_helper_error("status", &output)); } let stdout = String::from_utf8_lossy(&output.stdout); Ok(stdout.trim().eq_ignore_ascii_case("active")) } pub fn metrics(app: &AppHandle, profile_path: &Path) -> Result { let backend = bundled_backend(app)?; let mut command = Command::new(backend); command.arg("metrics").arg("--profile").arg(profile_path); #[cfg(target_os = "windows")] command.creation_flags(CREATE_NO_WINDOW); let output = command .output() .map_err(|err| format!("Unable to query embedded tunnel backend: {}", err))?; if !output.status.success() { return Err(format_helper_error("metrics", &output)); } serde_json::from_slice::(&output.stdout) .map_err(|err| format!("Unable to decode tunnel metrics: {}", err)) } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TunnelMetrics { pub active: bool, pub rx_bytes: u64, pub tx_bytes: u64, } fn bundled_backend(app: &AppHandle) -> Result { let resource_dir = app .path() .resource_dir() .map_err(|err| format!("Unable to resolve resource dir: {}", err))?; let relative = if cfg!(target_os = "windows") { PathBuf::from("bundled/windows-x64/nexavpn-tunnel-helper.exe") } else if cfg!(target_os = "macos") { PathBuf::from("bundled/macos-arm64/nexavpn-tunnel-helper") } else { return Err("This NexaVPN client build supports embedded tunnel backends only for Windows x64 and macOS ARM".into()); }; let path = resource_dir.join(relative); if !path.exists() { return Err("Embedded NexaVPN tunnel backend is not bundled in this build yet.".into()); } Ok(path) } fn format_helper_error(action: &str, output: &std::process::Output) -> String { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); let details = if !stderr.trim().is_empty() { stderr.trim() } else { stdout.trim() }; if details.is_empty() { return format!("NexaVPN tunnel backend {} failed with status {}", action, output.status); } details.to_string() }