From bbea4f8bd0ea68ef713f83c074227ac722745c87 Mon Sep 17 00:00:00 2001 From: nessi Date: Wed, 18 Mar 2026 07:53:38 +0100 Subject: [PATCH] feat: add idempotent tunnel connection with state polling and already-running detection Add tunnel_service_is_active check before Windows tunnel installation to skip if already running. Add is_already_running_error helper to detect "already installed and running" message in WireGuard output. Add wait_for_windows_tunnel_running that polls tunnel state up to 12 times with 500ms intervals after installation. Add describe_windows_tunnel_state for detailed error messages when tunnel fails to reach RUNNING state. --- desktop-client/src/App.tsx | 13 +++++ desktop-client/tunnel-helper/src/main.rs | 69 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/desktop-client/src/App.tsx b/desktop-client/src/App.tsx index 62284e8..5d22e75 100644 --- a/desktop-client/src/App.tsx +++ b/desktop-client/src/App.tsx @@ -29,6 +29,11 @@ function formatInvokeError(err: unknown, fallback: string) { return fallback; } +function isAlreadyRunningError(err: unknown) { + const message = formatInvokeError(err, ""); + return message.toLowerCase().includes("already installed and running"); +} + function currentProfileLabel(state: EnrollmentState | null) { if (!state) { return "Not provisioned"; @@ -189,6 +194,14 @@ export function App() { setError(null); } } catch (err) { + if (!connected && isAlreadyRunningError(err)) { + const active = await waitForTunnelStatus(true); + setConnected(active); + if (active) { + setError(null); + return; + } + } setError(formatInvokeError(err, "Tunnel action failed")); } } diff --git a/desktop-client/tunnel-helper/src/main.rs b/desktop-client/tunnel-helper/src/main.rs index 06c046b..81ecedc 100644 --- a/desktop-client/tunnel-helper/src/main.rs +++ b/desktop-client/tunnel-helper/src/main.rs @@ -508,6 +508,10 @@ fn is_windows_admin() -> Result { fn connect_direct(profile: &Path) -> Result<(), String> { #[cfg(target_os = "windows")] { + if tunnel_service_is_active(profile)? { + return Ok(()); + } + let wireguard = find_windows_wireguard()?; let output = Command::new(wireguard) .arg("/installtunnelservice") @@ -515,9 +519,13 @@ fn connect_direct(profile: &Path) -> Result<(), String> { .creation_flags(CREATE_NO_WINDOW) .output() .map_err(|err| format!("unable to launch WireGuard runtime: {err}"))?; + if is_already_running_error(&output) { + return Ok(()); + } if !output.status.success() { return Err(format_windows_runtime_error("connect", &output)); } + wait_for_windows_tunnel_running(profile)?; return Ok(()); } @@ -634,6 +642,59 @@ fn tunnel_service_is_active(profile: &Path) -> Result { Err("unsupported platform".into()) } +#[cfg(target_os = "windows")] +fn wait_for_windows_tunnel_running(profile: &Path) -> Result<(), String> { + for _ in 0..12 { + if tunnel_service_is_active(profile)? { + return Ok(()); + } + std::thread::sleep(Duration::from_millis(500)); + } + + Err(describe_windows_tunnel_state(profile)) +} + +#[cfg(target_os = "windows")] +fn describe_windows_tunnel_state(profile: &Path) -> String { + let tunnel_name = match tunnel_name(profile) { + Ok(value) => value, + Err(err) => return err, + }; + let service_name = format!("WireGuardTunnel${}", tunnel_name); + let output = Command::new("sc") + .arg("query") + .arg(&service_name) + .creation_flags(CREATE_NO_WINDOW) + .output(); + + match output { + Ok(output) if output.status.success() => { + let stdout = String::from_utf8_lossy(&output.stdout); + let summary = stdout + .lines() + .find(|line| line.contains("STATE")) + .map(str::trim) + .unwrap_or("state unavailable"); + format!("WireGuard tunnel service did not reach RUNNING state yet ({summary}).") + } + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let details = if !stderr.trim().is_empty() { + stderr.trim() + } else { + stdout.trim() + }; + if details.is_empty() { + "WireGuard tunnel service was created but could not be queried.".into() + } else { + format!("WireGuard tunnel service could not be queried: {details}") + } + } + Err(err) => format!("WireGuard tunnel service did not reach RUNNING state: {err}"), + } +} + fn tunnel_name(profile: &Path) -> Result<&str, String> { profile .file_stem() @@ -720,3 +781,11 @@ fn format_windows_runtime_error(action: &str, output: &Output) -> String { format!("WireGuard runtime {} failed: {}", action, details) } + +#[cfg(target_os = "windows")] +fn is_already_running_error(output: &Output) -> bool { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let combined = format!("{}\n{}", stdout, stderr).to_ascii_lowercase(); + combined.contains("already installed and running") +}