diff --git a/desktop-client/src-tauri/src/tunnel_manager.rs b/desktop-client/src-tauri/src/tunnel_manager.rs index 8dde6af..bef6133 100644 --- a/desktop-client/src-tauri/src/tunnel_manager.rs +++ b/desktop-client/src-tauri/src/tunnel_manager.rs @@ -61,6 +61,13 @@ pub fn is_active(app: &AppHandle, profile_path: &Path) -> Result { } pub fn metrics(app: &AppHandle, profile_path: &Path) -> Result { + #[cfg(target_os = "windows")] + { + if let Ok(metrics) = direct_windows_metrics(profile_path) { + return Ok(metrics); + } + } + let backend = bundled_backend(app)?; let mut command = Command::new(backend); command.arg("metrics").arg("--profile").arg(profile_path); @@ -86,6 +93,89 @@ pub struct TunnelMetrics { pub tx_bytes: u64, } +#[cfg(target_os = "windows")] +fn direct_windows_metrics(profile_path: &Path) -> Result { + let tunnel_name = profile_path + .file_stem() + .and_then(|value| value.to_str()) + .ok_or_else(|| "invalid profile filename".to_string())?; + let service_name = format!("WireGuardTunnel${}", tunnel_name); + + let mut status_cmd = Command::new("sc"); + status_cmd.arg("query").arg(&service_name).creation_flags(CREATE_NO_WINDOW); + let status_output = status_cmd + .output() + .map_err(|err| format!("Unable to query tunnel status: {err}"))?; + + if !status_output.status.success() { + return Ok(TunnelMetrics { + active: false, + rx_bytes: 0, + tx_bytes: 0, + }); + } + + let status_stdout = String::from_utf8_lossy(&status_output.stdout); + if !status_stdout.contains("RUNNING") { + return Ok(TunnelMetrics { + active: false, + rx_bytes: 0, + tx_bytes: 0, + }); + } + + let wg = find_windows_wg()?; + let mut wg_cmd = Command::new(wg); + wg_cmd + .arg("show") + .arg(tunnel_name) + .arg("dump") + .creation_flags(CREATE_NO_WINDOW); + let wg_output = wg_cmd + .output() + .map_err(|err| format!("Unable to query WireGuard counters: {err}"))?; + + if !wg_output.status.success() { + return Ok(TunnelMetrics { + active: true, + rx_bytes: 0, + tx_bytes: 0, + }); + } + + let stdout = String::from_utf8_lossy(&wg_output.stdout); + let mut rx_bytes = 0_u64; + let mut tx_bytes = 0_u64; + + for line in stdout.lines().skip(1) { + let fields: Vec<&str> = line.split('\t').collect(); + if fields.len() < 7 { + continue; + } + rx_bytes = rx_bytes.saturating_add(fields[5].parse::().unwrap_or(0)); + tx_bytes = tx_bytes.saturating_add(fields[6].parse::().unwrap_or(0)); + } + + Ok(TunnelMetrics { + active: true, + rx_bytes, + tx_bytes, + }) +} + +#[cfg(target_os = "windows")] +fn find_windows_wg() -> Result { + let candidates = [ + PathBuf::from(r"C:\Program Files\WireGuard\wg.exe"), + PathBuf::from(r"C:\Program Files (x86)\WireGuard\wg.exe"), + ]; + + candidates + .into_iter() + .find(|path| path.exists()) + .ok_or_else(|| "required Windows WireGuard CLI is not available".to_string()) +} + fn bundled_backend(app: &AppHandle) -> Result { let resource_dir = app .path()