feat: add automated WireGuard runtime installation for Windows desktop client with bundled MSI installer
Add install-runtime command to tunnel-helper for automated WireGuard installation on Windows. Download and bundle official WireGuard MSI during build process with automatic version discovery from wireguard.com. Add ensure_windows_runtime_installed checks before connect/disconnect operations. Implement install_windows_runtime with UAC elevation prompt and install_windows_runtime_direct for MS
This commit is contained in:
@@ -134,4 +134,5 @@ jobs:
|
||||
path: |
|
||||
desktop-client/src-tauri/target/x86_64-pc-windows-msvc/release/nexavpn-desktop.exe
|
||||
desktop-client/src-tauri/bundled/windows-x64/nexavpn-tunnel-helper.exe
|
||||
desktop-client/src-tauri/bundled/windows-x64/wireguard-installer.msi
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -78,6 +78,28 @@ else
|
||||
fi
|
||||
install -m 755 "${HELPER_DIR}/target/${TARGET}/release/${OUTPUT_NAME}" "${OUTPUT_DIR}/${OUTPUT_NAME}"
|
||||
|
||||
if [ "${TARGET}" = "x86_64-pc-windows-msvc" ]; then
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "curl is required to bundle the official WireGuard Windows MSI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MSI_URL="${NEXAVPN_WIREGUARD_WINDOWS_MSI_URL:-}"
|
||||
if [ -z "${MSI_URL}" ]; then
|
||||
INDEX_HTML="$(curl -fsSL https://download.wireguard.com/windows-client/)"
|
||||
MSI_NAME="$(printf '%s' "${INDEX_HTML}" | grep -oE 'wireguard-amd64-[0-9.]+\.msi' | sort -uV | tail -n 1)"
|
||||
if [ -z "${MSI_NAME}" ]; then
|
||||
echo "Unable to discover the latest official WireGuard Windows MSI."
|
||||
exit 1
|
||||
fi
|
||||
MSI_URL="https://download.wireguard.com/windows-client/${MSI_NAME}"
|
||||
fi
|
||||
|
||||
echo "Bundling WireGuard Windows runtime from ${MSI_URL}"
|
||||
rm -f "${OUTPUT_DIR}/wireguard-installer.msi"
|
||||
curl -fsSL "${MSI_URL}" -o "${OUTPUT_DIR}/wireguard-installer.msi"
|
||||
fi
|
||||
|
||||
if [ "${TARGET}" = "aarch64-apple-darwin" ]; then
|
||||
WG_BIN="$(command -v wg || true)"
|
||||
WG_QUICK_BIN="$(command -v wg-quick || true)"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_POSTINSTALL
|
||||
nsExec::ExecToLog '"$INSTDIR\bundled\windows-x64\nexavpn-tunnel-helper.exe" install-runtime'
|
||||
nsExec::ExecToLog '"$INSTDIR\bundled\windows-x64\nexavpn-tunnel-helper.exe" install-service'
|
||||
!macroend
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ fn run() -> Result<(), String> {
|
||||
{
|
||||
return match command.as_str() {
|
||||
"service" => run_windows_service_dispatcher(),
|
||||
"install-runtime" => install_windows_runtime(),
|
||||
"install-runtime-direct" => install_windows_runtime_direct(),
|
||||
"install-service" => install_windows_service(),
|
||||
"install-service-direct" => install_windows_service_direct(),
|
||||
"uninstall-service" => uninstall_windows_service(),
|
||||
@@ -332,6 +334,68 @@ fn install_windows_service() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn install_windows_runtime() -> Result<(), String> {
|
||||
if windows_wireguard_runtime_ready() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if is_windows_admin()? {
|
||||
return install_windows_runtime_direct();
|
||||
}
|
||||
|
||||
let helper_path = env::current_exe().map_err(|err| format!("Unable to resolve helper path: {err}"))?;
|
||||
let status = Command::new("powershell")
|
||||
.arg("-NoProfile")
|
||||
.arg("-Command")
|
||||
.arg(format!(
|
||||
"Start-Process -FilePath '{}' -ArgumentList 'install-runtime-direct' -Verb RunAs -Wait",
|
||||
helper_path.display()
|
||||
))
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.status()
|
||||
.map_err(|err| format!("Unable to request elevated WireGuard runtime install: {err}"))?;
|
||||
|
||||
if !status.success() {
|
||||
return Err("WireGuard runtime installation was cancelled or failed.".into());
|
||||
}
|
||||
|
||||
if windows_wireguard_runtime_ready() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err("WireGuard runtime installation finished, but the runtime is still unavailable.".into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn install_windows_runtime_direct() -> Result<(), String> {
|
||||
if windows_wireguard_runtime_ready() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let installer = bundled_windows_installer()
|
||||
.ok_or_else(|| "Bundled WireGuard Windows installer is missing from this NexaVPN build.".to_string())?;
|
||||
|
||||
let status = Command::new("msiexec.exe")
|
||||
.arg("/i")
|
||||
.arg(&installer)
|
||||
.arg("/qn")
|
||||
.arg("/norestart")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.status()
|
||||
.map_err(|err| format!("Unable to launch bundled WireGuard MSI: {err}"))?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(format!("Bundled WireGuard MSI installation failed with status {status}."));
|
||||
}
|
||||
|
||||
if windows_wireguard_runtime_ready() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err("WireGuard MSI completed, but wireguard.exe/wg.exe are still unavailable.".into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn install_windows_service_direct() -> Result<(), String> {
|
||||
let manager = ServiceManager::local_computer(
|
||||
@@ -506,6 +570,7 @@ fn is_windows_admin() -> Result<bool, String> {
|
||||
fn connect_direct(profile: &Path) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
ensure_windows_runtime_installed()?;
|
||||
if tunnel_service_is_active(profile)? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -551,6 +616,7 @@ fn connect_direct(profile: &Path) -> Result<(), String> {
|
||||
fn disconnect_direct(profile: &Path) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
ensure_windows_runtime_installed()?;
|
||||
let wireguard = find_windows_wireguard()?;
|
||||
let tunnel_name = profile
|
||||
.file_stem()
|
||||
@@ -811,6 +877,10 @@ fn parse_human_wireguard_bytes(value: &str) -> u64 {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn find_wg_cli() -> Result<PathBuf, String> {
|
||||
if let Some(path) = bundled_windows_tool("wg.exe") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let candidates = [
|
||||
PathBuf::from(r"C:\Program Files\WireGuard\wg.exe"),
|
||||
PathBuf::from(r"C:\Program Files (x86)\WireGuard\wg.exe"),
|
||||
@@ -820,7 +890,11 @@ fn find_wg_cli() -> Result<PathBuf, String> {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
Ok(PathBuf::from("wg.exe"))
|
||||
if command_exists_on_windows_path("wg.exe") {
|
||||
return Ok(PathBuf::from("wg.exe"));
|
||||
}
|
||||
|
||||
Err("required Windows wg.exe runtime is not available".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -880,6 +954,10 @@ fn macos_command_path_prefix(tool_path: &Path) -> String {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn find_windows_wireguard() -> Result<PathBuf, String> {
|
||||
if let Some(path) = bundled_windows_tool("wireguard.exe") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let candidates = [
|
||||
PathBuf::from(r"C:\Program Files\WireGuard\wireguard.exe"),
|
||||
PathBuf::from(r"C:\Program Files (x86)\WireGuard\wireguard.exe"),
|
||||
@@ -888,9 +966,50 @@ fn find_windows_wireguard() -> Result<PathBuf, String> {
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|path| path.exists())
|
||||
.or_else(|| command_exists_on_windows_path("wireguard.exe").then_some(PathBuf::from("wireguard.exe")))
|
||||
.ok_or_else(|| "required Windows tunnel runtime is not available".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn ensure_windows_runtime_installed() -> Result<(), String> {
|
||||
if windows_wireguard_runtime_ready() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
install_windows_runtime()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows_wireguard_runtime_ready() -> bool {
|
||||
find_windows_wireguard().is_ok() && find_wg_cli().is_ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn bundled_windows_tool(name: &str) -> Option<PathBuf> {
|
||||
let current_exe = env::current_exe().ok()?;
|
||||
let dir = current_exe.parent()?;
|
||||
let path = dir.join(name);
|
||||
path.exists().then_some(path)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn bundled_windows_installer() -> Option<PathBuf> {
|
||||
let current_exe = env::current_exe().ok()?;
|
||||
let dir = current_exe.parent()?;
|
||||
let path = dir.join("wireguard-installer.msi");
|
||||
path.exists().then_some(path)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn command_exists_on_windows_path(name: &str) -> bool {
|
||||
Command::new("where.exe")
|
||||
.arg(name)
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn format_windows_runtime_error(action: &str, output: &Output) -> String {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
Reference in New Issue
Block a user