diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-01-10 09:38:31 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-01-24 17:34:56 +0100 |
| commit | 1874bbe83ae48c943f061639cfd505bab6c26b34 (patch) | |
| tree | 64adfe2424fd175211258f97bc3084b49ab6f973 | |
| parent | 2fec6bb168e29ea47976b07c188d0cf792d70b77 (diff) | |
| download | mullvadvpn-1874bbe83ae48c943f061639cfd505bab6c26b34.tar.xz mullvadvpn-1874bbe83ae48c943f061639cfd505bab6c26b34.zip | |
Build wireguard-go via wireguard-go-rs on Windows
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | talpid-wireguard/src/ephemeral.rs | 5 | ||||
| -rw-r--r-- | talpid-wireguard/src/lib.rs | 17 | ||||
| -rw-r--r-- | talpid-wireguard/src/wireguard_go/mod.rs | 8 | ||||
| -rw-r--r-- | wireguard-go-rs/Cargo.toml | 6 | ||||
| -rw-r--r-- | wireguard-go-rs/build.rs | 277 | ||||
| -rw-r--r-- | wireguard-go-rs/src/lib.rs | 101 |
7 files changed, 382 insertions, 33 deletions
diff --git a/Cargo.lock b/Cargo.lock index 2bf2110f63..110b6b197a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5851,6 +5851,7 @@ dependencies = [ "log", "maybenot-ffi", "thiserror 2.0.9", + "windows-sys 0.52.0", "zeroize", ] diff --git a/talpid-wireguard/src/ephemeral.rs b/talpid-wireguard/src/ephemeral.rs index 1df0820014..1a61148487 100644 --- a/talpid-wireguard/src/ephemeral.rs +++ b/talpid-wireguard/src/ephemeral.rs @@ -145,10 +145,7 @@ async fn config_ephemeral_peers_inner( log::debug!("Successfully exchanged PSK with entry peer"); config.entry_peer.psk = entry_ephemeral_peer.psk; - #[cfg(not(target_os = "windows"))] - { - daita = entry_ephemeral_peer.daita; - } + daita = entry_ephemeral_peer.daita; } config.exit_peer_mut().psk = exit_ephemeral_peer.psk; diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs index 1b377e2f4f..205e150eee 100644 --- a/talpid-wireguard/src/lib.rs +++ b/talpid-wireguard/src/lib.rs @@ -692,6 +692,23 @@ impl WireguardMonitor { #[cfg(target_os = "windows")] { + #[cfg(wireguard_go)] + { + let use_userspace_wg = config.daita; + if use_userspace_wg { + log::debug!("Using userspace WireGuard implementation"); + let tunnel = Self::open_wireguard_go_tunnel( + runtime, + config, + log_path, + setup_done_tx, + route_manager, + ) + .map(Box::new)?; + return Ok(tunnel); + } + } + wireguard_nt::WgNtTunnel::start_tunnel(config, log_path, resource_dir, setup_done_tx) .map(|tun| Box::new(tun) as Box<dyn Tunnel + 'static>) .map_err(Error::TunnelError) diff --git a/talpid-wireguard/src/wireguard_go/mod.rs b/talpid-wireguard/src/wireguard_go/mod.rs index 2bfd8ef987..4698616d05 100644 --- a/talpid-wireguard/src/wireguard_go/mod.rs +++ b/talpid-wireguard/src/wireguard_go/mod.rs @@ -9,15 +9,18 @@ use crate::config::MULLVAD_INTERFACE_NAME; #[cfg(target_os = "android")] use crate::connectivity; use crate::logging::{clean_up_logging, initialize_logging}; +#[cfg(unix)] use ipnetwork::IpNetwork; #[cfg(daita)] use std::ffi::CString; +#[cfg(unix)] +use std::sync::{Arc, Mutex}; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; use std::{ future::Future, - os::unix::io::{AsRawFd, RawFd}, path::{Path, PathBuf}, pin::Pin, - sync::{Arc, Mutex}, }; #[cfg(target_os = "android")] use talpid_tunnel::tun_provider::Error as TunProviderError; @@ -27,6 +30,7 @@ use talpid_tunnel_config_client::DaitaSettings; use talpid_types::net::wireguard::PeerConfig; use talpid_types::BoxedError; +#[cfg(unix)] const MAX_PREPARE_TUN_ATTEMPTS: usize = 4; /// Maximum number of events that can be stored in the underlying buffer diff --git a/wireguard-go-rs/Cargo.toml b/wireguard-go-rs/Cargo.toml index cfaef554cc..b1388ab146 100644 --- a/wireguard-go-rs/Cargo.toml +++ b/wireguard-go-rs/Cargo.toml @@ -7,14 +7,16 @@ license.workspace = true [build-dependencies] anyhow = "1.0" -[target.'cfg(unix)'.dependencies] +[dependencies] thiserror.workspace = true log.workspace = true zeroize = "1.8.1" -[target.'cfg(not(target_os = "windows"))'.dependencies] # The app does not depend on maybenot-ffi itself, but adds it as a dependency to expose FFI symbols to wireguard-go. # This is done, instead of using the makefile in wireguard-go to build maybenot-ffi into its archive, to prevent # name clashes induced by link-time optimization. # NOTE: the version of maybenot-ffi below must be the same as the version checked into the wireguard-go submodule maybenot-ffi = "2.0.1" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52.0", features = ["Win32_NetworkManagement_Ndis"] } diff --git a/wireguard-go-rs/build.rs b/wireguard-go-rs/build.rs index 1148d4010e..ad485be28d 100644 --- a/wireguard-go-rs/build.rs +++ b/wireguard-go-rs/build.rs @@ -1,6 +1,8 @@ use std::{ borrow::BorrowMut, env, + fs::{self, File}, + io::{BufRead, BufReader, BufWriter, Write}, path::{Path, PathBuf}, process::Command, str, @@ -18,23 +20,24 @@ fn main() -> anyhow::Result<()> { println!("cargo::rerun-if-changed=libwg"); match target_os.as_str() { - "linux" => build_static_lib(Os::Linux, true)?, - "macos" => build_static_lib(Os::Macos, true)?, + "windows" => build_desktop_lib(Os::Windows, true)?, + "linux" => build_desktop_lib(Os::Linux, true)?, + "macos" => build_desktop_lib(Os::Macos, true)?, "android" => build_android_dynamic_lib(true)?, - // building wireguard-go-rs for windows is not implemented _ => {} } Ok(()) } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] enum Os { - Macos, + Windows, + MacOs, Linux, } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] enum Arch { Amd64, Arm64, @@ -64,7 +67,9 @@ impl AndroidTarget { fn host_os() -> anyhow::Result<Os> { // this ugliness is a limitation of rust, where we can't directly // access the target triple of the build script. - if cfg!(target_os = "linux") { + if cfg!(target_os = "windows") { + Ok(Os::Windows) + } else if cfg!(target_os = "linux") { Ok(Os::Linux) } else if cfg!(target_os = "macos") { Ok(Os::Macos) @@ -83,8 +88,8 @@ fn host_arch() -> anyhow::Result<Arch> { } } -/// Compile libwg as a static library and place it in `OUT_DIR`. -fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { +/// Compile libwg as a library and place it in `OUT_DIR`. +fn build_desktop_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { let out_dir = env::var("OUT_DIR").context("Missing OUT_DIR")?; let target_arch = env::var("CARGO_CFG_TARGET_ARCH").context("Missing 'CARGO_CFG_TARGET_ARCH")?; @@ -95,14 +100,8 @@ fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { _ => bail!("Unsupported architecture: {target_arch}"), }; - let out_file = format!("{out_dir}/libwg.a"); let mut go_build = Command::new("go"); - go_build - .args(["build", "-v", "-o", &out_file]) - .args(["-buildmode", "c-archive"]) - .args(if daita { &["--tags", "daita"][..] } else { &[] }) - .env("CGO_ENABLED", "1") - .current_dir("./libwg"); + go_build.env("CGO_ENABLED", "1").current_dir("./libwg"); // are we cross compiling? let cross_compiling = host_os()? != target_os || host_arch()? != target_arch; @@ -113,7 +112,58 @@ fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { }; match target_os { + Os::Windows => { + let target_dir = Path::new(&out_dir) + .ancestors() + .nth(3) + .context("Failed to find target dir")?; + + if daita { + build_shared_maybenot_lib(target_dir).context("Failed to build maybenot")?; + } + + let dll_path = target_dir.join("libwg.dll"); + + println!("cargo::rerun-if-changed={}", dll_path.display()); + println!( + "cargo::rerun-if-changed={}", + target_dir.join("libwg.lib").display() + ); + + go_build + .args(["build", "-v"]) + .arg("-o") + .arg(&dll_path) + .args(if daita { &["--tags", "daita"][..] } else { &[] }); + // Build dynamic lib + go_build.args(["-buildmode", "c-shared"]); + + go_build.env("GOOS", "windows"); + + generate_windows_lib(target_arch, target_dir)?; + + println!("cargo::rustc-link-search={}", target_dir.to_str().unwrap()); + println!("cargo::rustc-link-lib=dylib=libwg"); + + // Build using zig + match target_arch { + Arch::Amd64 => { + go_build.env("CC", "zig cc -target x86_64-windows"); + } + Arch::Arm64 => { + go_build.env("CC", "zig cc -target aarch64-windows"); + } + } + } Os::Linux => { + let out_file = format!("{out_dir}/libwg.a"); + go_build + .args(["build", "-v", "-o", &out_file]) + .args(if daita { &["--tags", "daita"][..] } else { &[] }); + + // Build static lib + go_build.args(["-buildmode", "c-archive"]); + go_build.env("GOOS", "linux"); if cross_compiling { @@ -122,8 +172,20 @@ fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { Arch::Amd64 => bail!("cross-compiling to linux x86_64 is not implemented"), }; } + + // make sure to link to the resulting binary + println!("cargo::rustc-link-search={out_dir}"); + println!("cargo::rustc-link-lib=static=wg"); } Os::Macos => { + let out_file = format!("{out_dir}/libwg.a"); + go_build + .args(["build", "-v", "-o", &out_file]) + .args(if daita { &["--tags", "daita"][..] } else { &[] }); + + // Build static lib + go_build.args(["-buildmode", "c-archive"]); + go_build.env("GOOS", "darwin"); if cross_compiling { @@ -144,15 +206,15 @@ fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { go_build.env("CGO_LDFLAGS", format!("-isysroot {sdkroot} -arch {c_arch}")); go_build.env("LD_LIBRARY_PATH", format!("{sdkroot}/usr/lib")); } + + // make sure to link to the resulting binary + println!("cargo::rustc-link-search={out_dir}"); + println!("cargo::rustc-link-lib=static=wg"); } } exec(go_build)?; - // make sure to link to the resulting binary - println!("cargo::rustc-link-search={out_dir}"); - println!("cargo::rustc-link-lib=static=wg"); - // if daita is enabled, also enable the corresponding rust feature flag if daita { println!(r#"cargo::rustc-cfg=daita"#); @@ -161,6 +223,181 @@ fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { Ok(()) } +// Build dynamically library for maybenot +fn build_shared_maybenot_lib(out_dir: impl AsRef<Path>) -> anyhow::Result<()> { + let target_triple = env::var("TARGET").context("Missing 'TARGET'")?; + let profile = env::var("PROFILE").context("Missing 'PROFILE'")?; + + let mut build_command = Command::new("cargo"); + + std::fs::create_dir_all("../build")?; + + let mut tmp_build_dir = Path::new("../build").canonicalize()?; + + // Strip \\?\ prefix. Note that doing this directly on Path/PathBuf fails + let path_str = tmp_build_dir.to_str().unwrap(); + if path_str.starts_with(r"\\?\") { + tmp_build_dir = PathBuf::from(&path_str[4..]); + } + + tmp_build_dir = tmp_build_dir.join("target"); + + build_command + .current_dir("./libwg/wireguard-go/maybenot/crates/maybenot-ffi") + .env("RUSTFLAGS", "-C metadata=maybenot-ffi -Ctarget-feature=+crt-static") + // Set temporary target dir to prevent deadlock + .env("CARGO_TARGET_DIR", &tmp_build_dir) + .arg("build") + .args(["--target", &target_triple]); + + exec(build_command)?; + + let artifacts_dir = tmp_build_dir.join(target_triple).join(profile); + + // Copy library to actual target dir + for filename in ["maybenot_ffi.dll", "maybenot_ffi.lib"] { + fs::copy( + artifacts_dir.join(filename), + out_dir.as_ref().join(filename), + ) + .with_context(|| format!("Failed to copy {filename}"))?; + } + + Ok(()) +} + +/// Generate a library for the exported functions. Required for linking. +/// This requires `lib.exe` in the path. +fn generate_windows_lib(arch: Arch, out_dir: impl AsRef<Path>) -> anyhow::Result<()> { + let exports_def_path = out_dir.as_ref().join("exports.def"); + generate_exports_def(&exports_def_path).context("Failed to generate exports.def")?; + generate_lib_from_exports_def(arch, &exports_def_path) + .context("Failed to generate lib from exports.def") +} + +fn find_lib_exe() -> anyhow::Result<PathBuf> { + let msbuild_exe = find_msbuild_exe()?; + + // Find lib.exe relative to msbuild.exe, in ../../../../ relative to msbuild + let search_path = msbuild_exe + .ancestors() + .nth(3) + .context("Unexpected msbuild.exe path")?; + + let path_is_lib_exe = |file: &Path| file.ends_with("Hostx64/x64/lib.exe"); + + find_file(search_path, &path_is_lib_exe)?.context("No lib.exe relative to msbuild.exe") +} + +/// Recursively search for file until 'condition' returns true +fn find_file( + dir: impl AsRef<Path>, + condition: &impl Fn(&Path) -> bool, +) -> anyhow::Result<Option<PathBuf>> { + for path in std::fs::read_dir(dir).context("Failed to read dir")? { + let entry = path.context("Failed to read dir entry")?; + let path = entry.path(); + if path.is_dir() { + if let Some(result) = find_file(&path, condition)? { + // TODO: distinguish between err and no result + return Ok(Some(result)); + } + } + if condition(&path) { + return Ok(Some(path.to_owned())); + } + } + Ok(None) +} + +/// Find msbuild.exe in PATH +fn find_msbuild_exe() -> anyhow::Result<PathBuf> { + let path = std::env::var_os("PATH").context("Missing PATH var")?; + std::env::split_paths(&path) + .find(|path| path.join("msbuild.exe").exists()) + .context("msbuild.exe not found in PATH") +} + +/// Generate exports.def from wireguard-go source +fn generate_lib_from_exports_def(arch: Arch, exports_path: impl AsRef<Path>) -> anyhow::Result<()> { + let lib_path = exports_path + .as_ref() + .parent() + .context("Missing parent")? + .join("libwg.lib"); + let path = exports_path.as_ref().to_str().context("Non-UTF8 path")?; + + let lib_exe = find_lib_exe()?; + + let mut lib_exe = Command::new(lib_exe); + lib_exe.args([ + format!("/def:{path}"), + format!("/out:{}", lib_path.to_str().context("Non-UTF8 lib path")?), + ]); + + match arch { + Arch::Amd64 => { + lib_exe.arg("/machine:X64"); + } + Arch::Arm64 => { + lib_exe.arg("/machine:ARM64"); + } + } + + exec(lib_exe)?; + + Ok(()) +} + +/// Generate exports.def from wireguard-go source +fn generate_exports_def(exports_path: impl AsRef<Path>) -> anyhow::Result<()> { + let file = File::create(exports_path).context("Failed to create file")?; + let mut file = BufWriter::new(file); + + writeln!(file, "LIBRARY libwg").context("Write LIBRARY statement")?; + writeln!(file, "EXPORTS").context("Write EXPORTS statement")?; + + let mut libwg_exports = vec![]; + for path in &[ + "./libwg/libwg.go", + "./libwg/libwg_windows.go", + "./libwg/libwg_daita.go", + ] { + libwg_exports.extend(gather_exports(path).context("Failed to find exports")?); + } + + for export in libwg_exports { + writeln!(file, "\t{export}").context("Failed to output exported function")?; + } + + Ok(()) +} + +/// Return functions exported from .go file +fn gather_exports(go_src_path: impl AsRef<Path>) -> anyhow::Result<Vec<String>> { + let go_src_path = go_src_path.as_ref(); + let mut exports = vec![]; + let file = File::open(go_src_path) + .with_context(|| format!("Failed to open go source: {}", go_src_path.display()))?; + + for line in BufReader::new(file).lines() { + let line = line.context("Failed to read line in go src")?; + let mut words = line.split_whitespace(); + + // Is this an export? + let Some("//export") = words.next() else { + continue; + }; + + let exported_func = words + .next() + .with_context(|| format!("Invalid export on line: {line}"))?; + exports.push(exported_func.to_owned()); + } + + Ok(exports) +} + /// Compile libwg as a dynamic library for android and place it in [`android_output_path`]. // NOTE: We use dynamic linking as Go cannot produce static binaries specifically for Android. fn build_android_dynamic_lib(daita: bool) -> anyhow::Result<()> { diff --git a/wireguard-go-rs/src/lib.rs b/wireguard-go-rs/src/lib.rs index 3e75506f61..4386c1f48a 100644 --- a/wireguard-go-rs/src/lib.rs +++ b/wireguard-go-rs/src/lib.rs @@ -6,18 +6,23 @@ //! //! The [`Tunnel`] type provides a safe Rust wrapper around the C FFI. -#![cfg(unix)] - +#[cfg(target_os = "windows")] +use core::mem::MaybeUninit; use core::{ ffi::{c_char, CStr}, - mem::{ManuallyDrop, MaybeUninit}, + mem::ManuallyDrop, slice, }; +#[cfg(target_os = "windows")] +use std::ffi::CString; use util::OnDrop; +#[cfg(target_os = "windows")] +use windows_sys::Win32::NetworkManagement::Ndis::NET_LUID_LH; use zeroize::Zeroize; mod util; +#[cfg(unix)] pub type Fd = std::os::unix::io::RawFd; pub type WgLogLevel = u32; @@ -34,6 +39,11 @@ use maybenot_ffi as _; pub struct Tunnel { /// wireguard-go handle to the tunnel. handle: i32, + + #[cfg(target_os = "windows")] + assigned_name: CString, + #[cfg(target_os = "windows")] + luid: NET_LUID_LH, } // NOTE: Must be kept in sync with libwg.go @@ -73,6 +83,7 @@ impl Tunnel { /// The `logging_callback` let's you provide a Rust function that receives any logging output /// from wireguard-go. `logging_context` is a value that will be passed to each invocation of /// `logging_callback`. + #[cfg(not(target_os = "windows"))] pub fn turn_on( #[cfg(not(target_os = "android"))] mtu: isize, settings: &CStr, @@ -96,6 +107,51 @@ impl Tunnel { Ok(Tunnel { handle: code }) } + /// Creates a new wireguard tunnel, uses the specific interface name, and file descriptors + /// for the tunnel device and logging. + /// + /// The `logging_callback` let's you provide a Rust function that receives any logging output + /// from wireguard-go. `logging_context` is a value that will be passed to each invocation of + /// `logging_callback`. + #[cfg(target_os = "windows")] + pub fn turn_on( + interface_name: &CStr, + mtu: u16, + settings: &CStr, + logging_callback: Option<LoggingCallback>, + logging_context: LoggingContext, + ) -> Result<Self, Error> { + // FIXME: use reasonable length + let mut assigned_name = [0u8; 128]; + let mut luid = MaybeUninit::uninit(); + + // SAFETY: pointers are valid for the the lifetime of this function + let code = unsafe { + ffi::wgTurnOn( + interface_name.as_ptr(), + assigned_name.as_mut_ptr() as *mut i8, + assigned_name.len(), + // SAFETY: This is a union of a u64 and `NET_LUID_LH_0` + luid.as_mut_ptr() as *mut u64, + mtu, + settings.as_ptr(), + logging_callback, + logging_context, + ) + }; + + result_from_code(code)?; + + let assigned_name = CStr::from_bytes_until_nul(&assigned_name).unwrap(); + + Ok(Tunnel { + handle: code, + assigned_name: assigned_name.to_owned(), + // SAFETY: wgTurnOn succeeded and the LUID is guaranteed to be intialized by wgTurnOn + luid: unsafe { luid.assume_init() }, + }) + } + /// Stop the wireguard tunnel. This also happens automatically if the [`Tunnel`] is dropped. pub fn turn_off(self) -> Result<(), Error> { // we manually turn off the tunnel here, so wrap it in ManuallyDrop to prevent the Drop @@ -105,6 +161,18 @@ impl Tunnel { result_from_code(code) } + /// Tunnel interface name + #[cfg(target_os = "windows")] + pub fn name(&self) -> &str { + self.assigned_name.to_str().expect("non-UTF8 name") + } + + /// Tunnel interface LUID + #[cfg(target_os = "windows")] + pub fn luid(&self) -> &NET_LUID_LH { + &self.luid + } + /// Special function for android multihop since that behavior is different from desktop /// and android non-multihop. /// @@ -276,7 +344,9 @@ impl Error { } mod ffi { - use super::{Fd, LoggingCallback, LoggingContext}; + #[cfg(not(target_os = "windows"))] + use super::Fd; + use super::{LoggingCallback, LoggingContext}; use core::ffi::{c_char, c_void}; extern "C" { @@ -286,14 +356,35 @@ mod ffi { /// /// Positive return values are tunnel handles for this specific wireguard tunnel instance. /// Negative return values signify errors. + #[cfg(any(target_os = "linux", target_os = "macos"))] + pub fn wgTurnOn( + mtu: isize, + settings: *const c_char, + fd: Fd, + logging_callback: Option<LoggingCallback>, + logging_context: LoggingContext, + ) -> i32; + + #[cfg(target_os = "android")] pub fn wgTurnOn( - #[cfg(not(target_os = "android"))] mtu: isize, settings: *const c_char, fd: Fd, logging_callback: Option<LoggingCallback>, logging_context: LoggingContext, ) -> i32; + #[cfg(target_os = "windows")] + pub fn wgTurnOn( + desired_name: *const c_char, + assigned_name: *mut c_char, + assigned_name_size: usize, + assigned_luid: *mut u64, + mtu: u16, + settings: *const c_char, + logging_callback: Option<LoggingCallback>, + logging_context: LoggingContext, + ) -> i32; + /// Creates a new wireguard tunnel, uses the specific interface name, and file descriptors /// for the tunnel device and logging. /// |
