Files
NexaVPN/desktop-client/src-tauri/src/tunnel_manager.rs
nessi d032950dfb refactor: replace metrics-based tunnel status check with direct status command in is_active function
Replace metrics query in is_active with direct tunnel_backend status command call to avoid unnecessary metrics overhead when only checking tunnel state. Parse status command stdout and compare against "active" string case-insensitively. Add Windows CREATE_NO_WINDOW flag to status command execution.
2026-03-18 09:56:42 +01:00

140 lines
4.3 KiB
Rust

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<bool, String> {
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<TunnelMetrics, String> {
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::<TunnelMetrics>(&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<PathBuf, String> {
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()
}