summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2024-12-05 13:49:38 +0100
committerDavid Lönnhager <david.l@mullvad.net>2024-12-10 14:34:02 +0100
commit930f24a7dc341027272eed73935cbbaa62041af6 (patch)
tree81bf9d2141dcb5d3896f8a8cc966da45db7c2a63
parent06b9a755512361ab16a9e8dcd6759108888a88e2 (diff)
downloadmullvadvpn-930f24a7dc341027272eed73935cbbaa62041af6.tar.xz
mullvadvpn-930f24a7dc341027272eed73935cbbaa62041af6.zip
Add windows-installer tool
-rw-r--r--Cargo.lock11
-rw-r--r--Cargo.toml40
-rw-r--r--windows-installer/Cargo.toml29
-rw-r--r--windows-installer/build.rs68
-rw-r--r--windows-installer/src/main.rs12
-rw-r--r--windows-installer/src/windows.rs145
-rw-r--r--windows-installer/windows-installer.manifest18
7 files changed, 323 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index dcb142fbd9..ca1240e694 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5267,6 +5267,17 @@ dependencies = [
]
[[package]]
+name = "windows-installer"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "mullvad-version",
+ "tempfile",
+ "windows-sys 0.52.0",
+ "winres",
+]
+
+[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 8c2d22a043..7e48d2ae28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,6 +43,46 @@ members = [
"talpid-wireguard",
"tunnel-obfuscation",
"wireguard-go-rs",
+ "windows-installer",
+]
+# The default members may exclude packages that cannot be built for all targets, or that do not always
+# need to be built
+default-members = [
+ "android/translations-converter",
+ "desktop/packages/nseventforwarder",
+ "mullvad-api",
+ "mullvad-cli",
+ "mullvad-daemon",
+ "mullvad-exclude",
+ "mullvad-fs",
+ "mullvad-ios",
+ "mullvad-jni",
+ "mullvad-management-interface",
+ "mullvad-nsis",
+ "mullvad-encrypted-dns-proxy",
+ "mullvad-paths",
+ "mullvad-problem-report",
+ "mullvad-relay-selector",
+ "mullvad-setup",
+ "mullvad-types",
+ "mullvad-types/intersection-derive",
+ "mullvad-version",
+ "talpid-core",
+ "talpid-dbus",
+ "talpid-future",
+ "talpid-macos",
+ "talpid-net",
+ "talpid-openvpn",
+ "talpid-openvpn-plugin",
+ "talpid-platform-metadata",
+ "talpid-routing",
+ "talpid-time",
+ "talpid-tunnel",
+ "talpid-tunnel-config-client",
+ "talpid-windows",
+ "talpid-wireguard",
+ "tunnel-obfuscation",
+ "wireguard-go-rs",
]
# Keep all lints in sync with `test/Cargo.toml`
diff --git a/windows-installer/Cargo.toml b/windows-installer/Cargo.toml
new file mode 100644
index 0000000000..5af6b492f9
--- /dev/null
+++ b/windows-installer/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "windows-installer"
+description = "Pack the Mullvad VPN installer for several platforms into a one executable"
+authors.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[lints]
+workspace = true
+
+[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.dependencies]
+windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemInformation", "Win32_System_Threading"] }
+tempfile = "3.10"
+anyhow = "1.0"
+
+[build-dependencies]
+winres = "0.1"
+anyhow = "1.0"
+windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemServices"] }
+mullvad-version = { path = "../mullvad-version" }
+
+[package.metadata.winres]
+ProductName = "Mullvad VPN"
+CompanyName = "Mullvad VPN AB"
+LegalCopyright = "(c) 2024 Mullvad VPN AB"
+InternalName = "mullvad-installer"
+OriginalFilename = "MullvadVPN.exe"
diff --git a/windows-installer/build.rs b/windows-installer/build.rs
new file mode 100644
index 0000000000..df3855e109
--- /dev/null
+++ b/windows-installer/build.rs
@@ -0,0 +1,68 @@
+use anyhow::Context;
+use std::{io, path::Path};
+
+const IDB_X64EXE: usize = 1;
+const IDB_ARM64EXE: usize = 2;
+
+fn main() -> anyhow::Result<()> {
+ if !std::env::var("TARGET")
+ .context("missing TARGET")?
+ .as_str()
+ .starts_with("x86_64-pc-windows-")
+ {
+ // This crate only makes sense on x64 Windows
+ return Ok(());
+ }
+
+ build_resource_rust_header().context("failed to write resource.rs")?;
+
+ let (Ok(x64_installer), Ok(arm64_installer)) = (
+ std::env::var("WIN_X64_INSTALLER"),
+ std::env::var("WIN_ARM64_INSTALLER"),
+ ) else {
+ eprintln!("Not building resource.rc - WIN_X64_INSTALLER and WIN_ARM64_INSTALLER not set");
+ // Linking must fail if the resource file isn't built
+ println!("cargo:rustc-link-lib=dylib=resource");
+ return Ok(());
+ };
+
+ let mut res = winres::WindowsResource::new();
+ res.append_rc_content(&format!(
+ r#"
+#define IDB_X64EXE {IDB_X64EXE}
+#define IDB_ARM64EXE {IDB_ARM64EXE}
+
+IDB_X64EXE BINARY "{x64_installer}"
+IDB_ARM64EXE BINARY "{arm64_installer}"
+"#
+ ));
+
+ res.set("ProductVersion", mullvad_version::VERSION);
+ res.set_icon("../dist-assets/icon.ico");
+ res.set_language(make_lang_id(
+ windows_sys::Win32::System::SystemServices::LANG_ENGLISH as u16,
+ windows_sys::Win32::System::SystemServices::SUBLANG_ENGLISH_US as u16,
+ ));
+
+ println!("cargo:rerun-if-changed=windows-installer.manifest");
+ res.set_manifest_file("windows-installer.manifest");
+ res.set("FileDescription", "Mullvad VPN installer");
+
+ res.compile().context("Failed to compile resources")
+}
+
+fn build_resource_rust_header() -> io::Result<()> {
+ let resource_header = Path::new(&std::env::var("OUT_DIR").unwrap()).join("resource.rs");
+ std::fs::write(
+ resource_header,
+ format!(
+ "pub const IDB_X64EXE: usize = {IDB_X64EXE};\n
+pub const IDB_ARM64EXE: usize = {IDB_ARM64EXE};\n"
+ ),
+ )
+}
+
+// Sourced from winnt.h: https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-makelangid
+fn make_lang_id(p: u16, s: u16) -> u16 {
+ (s << 10) | p
+}
diff --git a/windows-installer/src/main.rs b/windows-installer/src/main.rs
new file mode 100644
index 0000000000..62f80ee024
--- /dev/null
+++ b/windows-installer/src/main.rs
@@ -0,0 +1,12 @@
+#![windows_subsystem = "windows"]
+
+#[cfg(target_os = "windows")]
+#[path = "windows.rs"]
+mod imp;
+
+#[cfg(not(target_os = "windows"))]
+mod imp {
+ pub fn main() {}
+}
+
+pub use imp::*;
diff --git a/windows-installer/src/windows.rs b/windows-installer/src/windows.rs
new file mode 100644
index 0000000000..1b74b074b3
--- /dev/null
+++ b/windows-installer/src/windows.rs
@@ -0,0 +1,145 @@
+//! Universal Windows installer which contains both an x86 installer package and an ARM package.
+//! This can only be built for x86 Windows. This is because the installer must run on both x86 and
+//! ARM64, and x86 binaries can run on ARM64, but not vice versa.
+//!
+//! Building this requires two inputs into build.rs:
+//! * `WIN_X64_INSTALLER` - a path to the x64 Windows installer
+//! * `WIN_ARM64_INSTALLER` - a path to the ARM64 Windows installer
+use anyhow::{bail, Context};
+use std::{
+ ffi::{c_ushort, OsStr},
+ io::{self, Write},
+ num::NonZero,
+ process::{Command, ExitStatus},
+ ptr::NonNull,
+};
+use tempfile::TempPath;
+use windows_sys::{
+ w,
+ Win32::System::{
+ LibraryLoader::{FindResourceW, LoadResource, LockResource, SizeofResource},
+ SystemInformation::{IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM64},
+ Threading::IsWow64Process2,
+ },
+};
+
+/// Import resource constants from `resource.rs`. This is automatically generated by the build
+/// script. See the [module-level documentation](crate).
+mod resource {
+ include!(concat!(env!("OUT_DIR"), "/resource.rs"));
+}
+
+pub fn main() -> anyhow::Result<()> {
+ let architecture = get_native_arch()?;
+ let exe_data = find_binary_data(architecture)?;
+ let path = write_file_to_temp(exe_data)?;
+
+ let status = run_with_forwarded_args(&path).context("Failed to run unpacked installer")?;
+
+ // We cannot rely on drop here since we need to `exit`, so remove explicitly
+ if let Err(error) = std::fs::remove_file(path) {
+ eprintln!("Failed to remove unpacked installer: {error}");
+ }
+
+ std::process::exit(status.code().unwrap());
+}
+
+/// Run path and pass all arguments from `argv[1..]` to it
+fn run_with_forwarded_args(path: impl AsRef<OsStr>) -> io::Result<ExitStatus> {
+ let mut command = Command::new(path);
+
+ let args = std::env::args().skip(1);
+ command.args(args).status()
+}
+
+/// Write file to a temporary file and return its path
+fn write_file_to_temp(data: &[u8]) -> anyhow::Result<TempPath> {
+ let mut file = tempfile::NamedTempFile::new().context("Failed to create tempfile")?;
+ file.write_all(data)
+ .context("Failed to extract temporary installer")?;
+ Ok(file.into_temp_path())
+}
+
+/// Return a slice of data for the given resource
+fn find_binary_data(architecture: Architecture) -> anyhow::Result<&'static [u8]> {
+ let resource_id = match architecture {
+ Architecture::X64 => resource::IDB_X64EXE,
+ Architecture::Arm64 => resource::IDB_ARM64EXE,
+ };
+
+ let Some(resource_info) =
+ // SAFETY: Looks unsafe but is actually safe. The cast is equivalent to `MAKEINTRESOURCE`,
+ // which is not available in windows-sys, as it is a macro.
+ // `resource_id` is guaranteed by the build script to refer to an actual resource.
+ // See https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-findresourcew
+ NonZero::new(unsafe { FindResourceW(0, resource_id as _, w!("BINARY")) })
+ else {
+ bail!("Failed to find resource: {}", io::Error::last_os_error());
+ };
+
+ // SAFETY: We have a valid resource info handle
+ // NOTE: Resources loaded with LoadResource should not be freed.
+ // See https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadresource
+ let Some(resource) = NonNull::new(unsafe { LoadResource(0, resource_info.get()) }) else {
+ bail!("Failed to load resource: {}", io::Error::last_os_error());
+ };
+
+ // SAFETY: We have a valid resource info handle
+ let Some(resource_size) = NonZero::new(unsafe { SizeofResource(0, resource_info.get()) })
+ else {
+ bail!(
+ "Failed to get resource size: {}",
+ io::Error::last_os_error()
+ );
+ };
+
+ // SAFETY: We have a valid resource info handle
+ // NOTE: We do not need to unload this handle, because it doesn't actually lock anything.
+ // See https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-lockresource
+ let Some(resource_data) = NonNull::new(unsafe { LockResource(resource.as_ptr()) }) else {
+ bail!(
+ "Failed to get resource data: {}",
+ io::Error::last_os_error()
+ );
+ };
+
+ debug_assert!(resource_data.is_aligned());
+
+ // SAFETY: The pointer is non-null, valid and constant for the remainder of the process lifetime
+ let resource_slice = unsafe {
+ std::slice::from_raw_parts(
+ resource_data.as_ptr() as *const u8,
+ usize::try_from(resource_size.get()).unwrap(),
+ )
+ };
+
+ Ok(resource_slice)
+}
+
+#[derive(Debug)]
+enum Architecture {
+ X64,
+ Arm64,
+}
+
+/// Return native architecture (ignoring WOW64)
+fn get_native_arch() -> anyhow::Result<Architecture> {
+ let mut running_arch: c_ushort = 0;
+ let mut native_arch: c_ushort = 0;
+
+ // SAFETY: Trivially safe, since we provide the required arguments. `hprocess == 0` is
+ // undocumented but refers to the current process.
+ let result = unsafe { IsWow64Process2(0, &mut running_arch, &mut native_arch) };
+ if result == 0 {
+ bail!(
+ "Failed to get native architecture: {}",
+ io::Error::last_os_error()
+ );
+ }
+
+ match native_arch {
+ IMAGE_FILE_MACHINE_AMD64 => Ok(Architecture::X64),
+ IMAGE_FILE_MACHINE_ARM64 => Ok(Architecture::Arm64),
+ other => bail!("unsupported architecture: {other}"),
+ }
+}
diff --git a/windows-installer/windows-installer.manifest b/windows-installer/windows-installer.manifest
new file mode 100644
index 0000000000..f640b7c615
--- /dev/null
+++ b/windows-installer/windows-installer.manifest
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
+ <security>
+ <requestedPrivileges>
+ <requestedExecutionLevel
+ level="requireAdministrator"
+ uiAccess="false"/>
+ </requestedPrivileges>
+ </security>
+</trustInfo>
+<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!--The ID below indicates app support for Windows 10 and Windows 11 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ </application>
+</compatibility>
+</assembly>