feat: add direct WireGuard metrics collection on Windows with service status check and wg show dump parsing
Add direct_windows_metrics function that queries WireGuard tunnel metrics directly using sc query and wg show dump commands instead of tunnel helper. Add find_windows_wg helper to locate wg.exe in standard installation paths. Update metrics function to attempt direct collection first on Windows before falling back to tunnel helper. Parse rx_bytes and tx_bytes from wg show dump output by sum
This commit is contained in:
@@ -61,6 +61,13 @@ pub fn is_active(app: &AppHandle, profile_path: &Path) -> Result<bool, String> {
|
||||
}
|
||||
|
||||
pub fn metrics(app: &AppHandle, profile_path: &Path) -> Result<TunnelMetrics, String> {
|
||||
#[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<TunnelMetrics, String> {
|
||||
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::<u64>().unwrap_or(0));
|
||||
tx_bytes = tx_bytes.saturating_add(fields[6].parse::<u64>().unwrap_or(0));
|
||||
}
|
||||
|
||||
Ok(TunnelMetrics {
|
||||
active: true,
|
||||
rx_bytes,
|
||||
tx_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn find_windows_wg() -> Result<PathBuf, String> {
|
||||
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<PathBuf, String> {
|
||||
let resource_dir = app
|
||||
.path()
|
||||
|
||||
Reference in New Issue
Block a user