diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-01-24 17:35:39 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-01-24 17:35:39 +0100 |
| commit | 654de1cd2d3cdde6a3c26fe0cf5f26a4b0d1a89c (patch) | |
| tree | 05a467738ba56f6c1affeefaaffddc41deba2121 | |
| parent | ff88a777e7c73dd1b6ea995df517d37bb26cc7e0 (diff) | |
| parent | 0d5ba1a5b6ff2d3b8b37d36cd4997082a7ac5dcb (diff) | |
| download | mullvadvpn-654de1cd2d3cdde6a3c26fe0cf5f26a4b0d1a89c.tar.xz mullvadvpn-654de1cd2d3cdde6a3c26fe0cf5f26a4b0d1a89c.zip | |
Merge branch 'win-daita-v2'
29 files changed, 813 insertions, 202 deletions
diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index ec021eb956..ec1ccb030e 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -63,8 +63,17 @@ jobs: sudo apt-get update sudo apt-get install libdbus-1-dev + - name: Install msbuild + if: matrix.os == 'windows-latest' + uses: microsoft/setup-msbuild@v1.0.2 + with: + vs-version: 16 + + - name: Install latest zig + if: matrix.os == 'windows-latest' + uses: mlugg/setup-zig@v1 + - name: Install Go - if: matrix.os == 'linux-latest' || matrix.os == 'macos-latest' uses: actions/setup-go@v5 with: go-version: 1.21.3 diff --git a/.github/workflows/daemon.yml b/.github/workflows/daemon.yml index 2727d6697b..7977d8ab68 100644 --- a/.github/workflows/daemon.yml +++ b/.github/workflows/daemon.yml @@ -130,7 +130,9 @@ jobs: uses: actions/checkout@v4 - name: Checkout submodules - run: git submodule update --init --depth=1 + run: | + git submodule update --init --depth=1 + git submodule update --init --recursive --depth=1 wireguard-go-rs - name: Install Protoc # NOTE: ARM runner already has protoc @@ -183,6 +185,9 @@ jobs: with: vs-version: 16 + - name: Install latest zig + uses: mlugg/setup-zig@v1 + - name: Build Windows modules if: steps.cache-windows-modules.outputs.cache-hit != 'true' shell: bash diff --git a/.github/workflows/desktop-e2e.yml b/.github/workflows/desktop-e2e.yml index 20f2f96926..a23cdbd602 100644 --- a/.github/workflows/desktop-e2e.yml +++ b/.github/workflows/desktop-e2e.yml @@ -200,6 +200,8 @@ jobs: toolchain: stable target: i686-pc-windows-msvc default: true + - name: Install latest zig + uses: mlugg/setup-zig@v1 - name: Install msbuild uses: microsoft/setup-msbuild@v1.0.2 with: diff --git a/.github/workflows/rust-unused-dependencies.yml b/.github/workflows/rust-unused-dependencies.yml index 69e253231b..eba2735f9c 100644 --- a/.github/workflows/rust-unused-dependencies.yml +++ b/.github/workflows/rust-unused-dependencies.yml @@ -103,11 +103,21 @@ jobs: uses: actions/checkout@v4 - name: Checkout wireguard-go submodule - if: matrix.os == 'macos-latest' run: | git config --global --add safe.directory '*' + git submodule update --init --depth=1 git submodule update --init --recursive --depth=1 wireguard-go-rs + - name: Install msbuild + if: matrix.os == 'windows-latest' + uses: microsoft/setup-msbuild@v1.0.2 + with: + vs-version: 16 + + - name: Install latest zig + if: matrix.os == 'windows-latest' + uses: mlugg/setup-zig@v1 + - name: Install Protoc uses: arduino/setup-protoc@v3 with: diff --git a/.gitignore b/.gitignore index e4ec60fcf0..2b1d8753f1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ /dist-assets/mullvad-setup.exe /dist-assets/mullvad-problem-report /dist-assets/mullvad-problem-report.exe +/dist-assets/libwg.dll +/dist-assets/maybenot_ffi.dll /dist-assets/libtalpid_openvpn_plugin.dylib /dist-assets/libtalpid_openvpn_plugin.so /dist-assets/talpid_openvpn_plugin.dll diff --git a/BuildInstructions.md b/BuildInstructions.md index d3cf347c9c..b81ba0d9c7 100644 --- a/BuildInstructions.md +++ b/BuildInstructions.md @@ -23,9 +23,8 @@ on your platform please submit an issue or a pull request. Install the `msi` hosted here: https://github.com/volta-cli/volta -- (Not Windows) Install Go (ideally version `1.21`) by following the [official - instructions](https://golang.org/doc/install). Newer versions may work - too. +- Install Go (ideally version `1.21`) by following the [official instructions](https://golang.org/doc/install). + Newer versions may work too. - Install a protobuf compiler (version 3.15 and up), it can be installed on most major Linux distros via the package name `protobuf-compiler`, `protobuf` on macOS via Homebrew, and on Windows @@ -96,6 +95,8 @@ The host has to have the following installed: - `bash` installed as well as a few base unix utilities, including `sed` and `tail`. You are recommended to use [Git for Windows]. +- `zig` installed and available in `%PATH%`. 0.14 or later is recommended: https://ziglang.org/download/. + - `msbuild.exe` available in `%PATH%`. If you installed Visual Studio Community edition, the binary can be found under: @@ -153,7 +154,7 @@ In addition to the above requirements: the Electron app: ``` - pushd gui + pushd desktop/packages/mullvad-vpn npm install --target_arch=x64 grpc-tools popd ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index b47012e172..1508a76106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Line wrap the file at 100 chars. Th ### Added #### Windows - Add support for Windows ARM64. +- Add support for DAITA V2. +- Add back wireguard-go (userspace WireGuard) support. ### Changed - (Linux and macOS only) Update to DAITA v2. The main difference is that many different machines are 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", ] @@ -265,6 +265,8 @@ function build { mullvad-problem-report.exe talpid_openvpn_plugin.dll mullvad-setup.exe + libwg.dll + maybenot_ffi.dll ) fi diff --git a/desktop/packages/mullvad-vpn/tasks/distribution.js b/desktop/packages/mullvad-vpn/tasks/distribution.js index d05fdd4a69..8ab8a35882 100644 --- a/desktop/packages/mullvad-vpn/tasks/distribution.js +++ b/desktop/packages/mullvad-vpn/tasks/distribution.js @@ -177,7 +177,8 @@ function newConfig() { ), to: '.', }, - { from: distAssets('maybenot_machines'), to: '.' }, + { from: distAssets(path.join('${env.DIST_SUBDIR}', 'libwg.dll')), to: '.' }, + { from: distAssets(path.join('${env.DIST_SUBDIR}', 'maybenot_ffi.dll')), to: '.' }, ], }, diff --git a/talpid-tunnel-config-client/src/lib.rs b/talpid-tunnel-config-client/src/lib.rs index 381bc65a53..5d2d785dba 100644 --- a/talpid-tunnel-config-client/src/lib.rs +++ b/talpid-tunnel-config-client/src/lib.rs @@ -22,7 +22,6 @@ mod proto { tonic::include_proto!("ephemeralpeer"); } -#[cfg(unix)] const DAITA_VERSION: u32 = 2; #[derive(Debug)] @@ -88,7 +87,6 @@ pub const CONFIG_SERVICE_PORT: u16 = 1337; pub struct EphemeralPeer { pub psk: Option<PresharedKey>, - #[cfg(unix)] pub daita: Option<DaitaSettings>, } @@ -141,19 +139,15 @@ pub async fn request_ephemeral_peer_with( wg_parent_pubkey: parent_pubkey.as_bytes().to_vec(), wg_ephemeral_peer_pubkey: ephemeral_pubkey.as_bytes().to_vec(), post_quantum: pq_request, - #[cfg(windows)] - daita: Some(proto::DaitaRequestV1 { - activate_daita: enable_daita, - }), - #[cfg(windows)] - daita_v2: None, - #[cfg(unix)] daita: None, - #[cfg(unix)] - daita_v2: enable_daita.then(|| proto::DaitaRequestV2 { - level: i32::from(proto::DaitaLevel::LevelDefault), - platform: i32::from(get_platform()), - version: DAITA_VERSION, + daita_v2: enable_daita.then(|| { + let platform = get_platform(); + log::trace!("DAITA v2 platform: {platform:?}"); + proto::DaitaRequestV2 { + level: i32::from(proto::DaitaLevel::LevelDefault), + platform: i32::from(platform), + version: DAITA_VERSION, + } }), }) .await @@ -204,30 +198,22 @@ pub async fn request_ephemeral_peer_with( None }; - #[cfg(unix)] - { - let daita = response.daita.map(|daita| DaitaSettings { - client_machines: daita.client_machines, - max_padding_frac: daita.max_padding_frac, - max_blocking_frac: daita.max_blocking_frac, - }); - if daita.is_none() && enable_daita { - return Err(Error::MissingDaitaResponse); - } - Ok(EphemeralPeer { psk, daita }) - } - - #[cfg(windows)] - { - Ok(EphemeralPeer { psk }) + let daita = response.daita.map(|daita| DaitaSettings { + client_machines: daita.client_machines, + max_padding_frac: daita.max_padding_frac, + max_blocking_frac: daita.max_blocking_frac, + }); + if daita.is_none() && enable_daita { + return Err(Error::MissingDaitaResponse); } + Ok(EphemeralPeer { psk, daita }) } -#[cfg(unix)] const fn get_platform() -> proto::DaitaPlatform { use proto::DaitaPlatform; const PLATFORM: DaitaPlatform = if cfg!(target_os = "windows") { - DaitaPlatform::WindowsNative + // FIXME: wggo + DaitaPlatform::LinuxWgGo } else if cfg!(target_os = "linux") { DaitaPlatform::LinuxWgGo } else if cfg!(target_os = "macos") { diff --git a/talpid-wireguard/Cargo.toml b/talpid-wireguard/Cargo.toml index 3a19f5a70a..6341c02bac 100644 --- a/talpid-wireguard/Cargo.toml +++ b/talpid-wireguard/Cargo.toml @@ -30,8 +30,6 @@ tunnel-obfuscation = { path = "../tunnel-obfuscation" } rand = "0.8.5" surge-ping = "0.8.0" rand_chacha = "0.3.1" - -[target.'cfg(not(windows))'.dependencies] wireguard-go-rs = { path = "../wireguard-go-rs"} [target.'cfg(target_os="android")'.dependencies] diff --git a/talpid-wireguard/build.rs b/talpid-wireguard/build.rs index ab3500330c..23c2f3bb67 100644 --- a/talpid-wireguard/build.rs +++ b/talpid-wireguard/build.rs @@ -6,11 +6,10 @@ fn main() { if target_os == "windows" { declare_libs_dir("../dist-assets/binaries"); } - // Wireguard-Go can be used on all platforms except Windows + // Wireguard-Go can be used on all platforms println!("cargo::rustc-check-cfg=cfg(wireguard_go)"); - if matches!(target_os.as_str(), "linux" | "macos" | "android") { - println!("cargo::rustc-cfg=wireguard_go"); - } + println!("cargo::rustc-cfg=wireguard_go"); + // Enable DAITA by default on desktop and android println!("cargo::rustc-check-cfg=cfg(daita)"); println!("cargo::rustc-cfg=daita"); diff --git a/talpid-wireguard/src/connectivity/mock.rs b/talpid-wireguard/src/connectivity/mock.rs index 5b7c98b183..8149e0ced3 100644 --- a/talpid-wireguard/src/connectivity/mock.rs +++ b/talpid-wireguard/src/connectivity/mock.rs @@ -121,7 +121,7 @@ impl Tunnel for MockTunnel { #[cfg(daita)] fn start_daita( &mut self, - #[cfg(not(target_os = "windows"))] _: talpid_tunnel_config_client::DaitaSettings, + _: talpid_tunnel_config_client::DaitaSettings, ) -> std::result::Result<(), TunnelError> { Ok(()) } diff --git a/talpid-wireguard/src/ephemeral.rs b/talpid-wireguard/src/ephemeral.rs index 1df0820014..1d7f4f3955 100644 --- a/talpid-wireguard/src/ephemeral.rs +++ b/talpid-wireguard/src/ephemeral.rs @@ -110,7 +110,6 @@ async fn config_ephemeral_peers_inner( ) .await?; - #[cfg(not(target_os = "windows"))] let mut daita = exit_ephemeral_peer.daita; log::debug!("Retrieved ephemeral peer"); @@ -145,14 +144,10 @@ 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; - #[cfg(daita)] if config.daita { log::trace!("Enabling constant packet size for entry peer"); config.entry_peer.constant_packet_size = true; @@ -170,28 +165,18 @@ async fn config_ephemeral_peers_inner( ) .await?; - #[cfg(daita)] if config.daita { - #[cfg(not(target_os = "windows"))] - let Some(daita) = daita - else { + let Some(daita) = daita else { unreachable!("missing DAITA settings"); }; // Start local DAITA machines let mut tunnel = tunnel.lock().await; if let Some(tunnel) = tunnel.as_mut() { - #[cfg(not(target_os = "windows"))] tunnel .start_daita(daita) .map_err(Error::TunnelError) .map_err(CloseMsg::SetupError)?; - - #[cfg(target_os = "windows")] - tunnel - .start_daita() - .map_err(Error::TunnelError) - .map_err(CloseMsg::SetupError)?; } } diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs index 1b377e2f4f..330a2c76cf 100644 --- a/talpid-wireguard/src/lib.rs +++ b/talpid-wireguard/src/lib.rs @@ -18,7 +18,7 @@ use std::{ pin::Pin, sync::{mpsc as sync_mpsc, Arc, Mutex}, }; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "windows"))] use std::{env, sync::LazyLock}; #[cfg(not(target_os = "android"))] use talpid_routing::{self, RequiredRoute}; @@ -28,7 +28,7 @@ use talpid_tunnel::{ tun_provider::TunProvider, EventHook, TunnelArgs, TunnelEvent, TunnelMetadata, }; -#[cfg(not(target_os = "windows"))] +#[cfg(daita)] use talpid_tunnel_config_client::DaitaSettings; use talpid_types::{ net::{wireguard::TunnelParameters, AllowedTunnelTraffic, Endpoint, TransportProtocol}, @@ -149,7 +149,7 @@ pub struct WireguardMonitor { obfuscator: Arc<AsyncMutex<Option<ObfuscatorHandle>>>, } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "windows"))] /// Overrides the preference for the kernel module for WireGuard. static FORCE_USERSPACE_WIREGUARD: LazyLock<bool> = LazyLock::new(|| { env::var("TALPID_FORCE_USERSPACE_WIREGUARD") @@ -299,7 +299,6 @@ impl WireguardMonitor { let config = config.clone(); let iface_name = iface_name.clone(); tokio::task::spawn(async move { - #[cfg(daita)] if config.daita { // TODO: For now, we assume the MTU during the tunnel lifetime. // We could instead poke maybenot whenever we detect changes to it. @@ -692,12 +691,29 @@ impl WireguardMonitor { #[cfg(target_os = "windows")] { + #[cfg(wireguard_go)] + { + let use_userspace_wg = config.daita || *FORCE_USERSPACE_WIREGUARD; + if use_userspace_wg { + log::debug!("Using userspace WireGuard implementation"); + let tunnel = runtime + .block_on(Self::open_wireguard_go_tunnel( + 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) } - #[cfg(wireguard_go)] + #[cfg(all(wireguard_go, not(target_os = "windows")))] { #[cfg(target_os = "linux")] log::debug!("Using userspace WireGuard implementation"); @@ -721,23 +737,25 @@ impl WireguardMonitor { async fn open_wireguard_go_tunnel( config: &Config, log_path: Option<&Path>, - tun_provider: Arc<Mutex<TunProvider>>, + #[cfg(unix)] tun_provider: Arc<Mutex<TunProvider>>, + #[cfg(windows)] setup_done_tx: mpsc::Sender<std::result::Result<(), BoxedError>>, + #[cfg(windows)] route_manager: talpid_routing::RouteManagerHandle, #[cfg(target_os = "android")] gateway_only: bool, #[cfg(target_os = "android")] cancel_receiver: connectivity::CancelReceiver, ) -> Result<WgGoTunnel> { + #[cfg(unix)] let routes = config .get_tunnel_destinations() .flat_map(Self::replace_default_prefixes); - #[cfg(not(target_os = "android"))] - let tunnel = WgGoTunnel::start_tunnel( - #[allow(clippy::needless_borrow)] - &config, - log_path, - tun_provider, - routes, - ) - .map_err(Error::TunnelError)?; + #[cfg(all(unix, not(target_os = "android")))] + let tunnel = WgGoTunnel::start_tunnel(config, log_path, tun_provider, routes) + .map_err(Error::TunnelError)?; + + #[cfg(target_os = "windows")] + let tunnel = WgGoTunnel::start_tunnel(config, log_path, route_manager, setup_done_tx) + .await + .map_err(Error::TunnelError)?; // Android uses multihop implemented in Mullvad's wireguard-go fork. When negotiating // with an ephemeral peer, this multihop strategy require us to restart the tunnel @@ -1018,10 +1036,7 @@ pub(crate) trait Tunnel: Send + Sync { ) -> Pin<Box<dyn Future<Output = std::result::Result<(), TunnelError>> + Send + 'a>>; #[cfg(daita)] /// A [`Tunnel`] capable of using DAITA. - #[cfg(not(target_os = "windows"))] fn start_daita(&mut self, settings: DaitaSettings) -> std::result::Result<(), TunnelError>; - #[cfg(target_os = "windows")] - fn start_daita(&mut self) -> std::result::Result<(), TunnelError>; } /// Errors to be returned from WireGuard implementations, namely implementers of the Tunnel trait diff --git a/talpid-wireguard/src/wireguard_go/mod.rs b/talpid-wireguard/src/wireguard_go/mod.rs index 2bfd8ef987..a304565967 100644 --- a/talpid-wireguard/src/wireguard_go/mod.rs +++ b/talpid-wireguard/src/wireguard_go/mod.rs @@ -9,24 +9,30 @@ 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::os::unix::io::{AsRawFd, RawFd}; +#[cfg(unix)] +use std::sync::{Arc, Mutex}; 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; +#[cfg(not(target_os = "windows"))] use talpid_tunnel::tun_provider::{Tun, TunProvider}; +#[cfg(daita)] use talpid_tunnel_config_client::DaitaSettings; #[cfg(target_os = "android")] 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 @@ -161,6 +167,7 @@ pub(crate) struct WgGoTunnelState { tunnel_handle: wireguard_go_rs::Tunnel, // holding on to the tunnel device and the log file ensures that the associated file handles // live long enough and get closed when the tunnel is stopped + #[cfg(unix)] _tunnel_device: Tun, // context that maps to fs::File instance and stores the file path, used with logging callback _logging_context: LoggingContext, @@ -171,6 +178,10 @@ pub(crate) struct WgGoTunnelState { /// This is used to cancel the connectivity checks that occur when toggling multihop #[cfg(target_os = "android")] cancel_receiver: connectivity::CancelReceiver, + /// Default route change callback. This is used to rebind the endpoint socket when the default + /// route (network) is changed. + #[cfg(target_os = "windows")] + _socket_update_cb: Option<talpid_routing::CallbackHandle>, } impl WgGoTunnelState { @@ -210,7 +221,7 @@ impl WgGoTunnelState { } impl WgGoTunnel { - #[cfg(not(target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "macos"))] pub fn start_tunnel( config: &Config, log_path: Option<&Path>, @@ -248,6 +259,92 @@ impl WgGoTunnel { })) } + #[cfg(target_os = "windows")] + pub async fn start_tunnel( + config: &Config, + log_path: Option<&Path>, + route_manager: talpid_routing::RouteManagerHandle, + mut setup_done_tx: futures::channel::mpsc::Sender<std::result::Result<(), BoxedError>>, + ) -> Result<Self> { + use futures::SinkExt; + use talpid_types::ErrorExt; + + let wg_config_str = config.to_userspace_format(); + let logging_context = initialize_logging(log_path) + .map(|ordinal| LoggingContext::new(ordinal, log_path.map(Path::to_owned))) + .map_err(TunnelError::LoggingError)?; + + let socket_update_cb = route_manager + .add_default_route_change_callback(Box::new(Self::default_route_changed_callback)) + .await + .ok(); + if socket_update_cb.is_none() { + log::warn!("Failed to register default route callback"); + } + + let handle = wireguard_go_rs::Tunnel::turn_on( + c"Mullvad", + config.mtu, + &wg_config_str, + Some(logging::wg_go_logging_callback), + logging_context.ordinal, + ) + .map_err(|e| TunnelError::FatalStartWireguardError(Box::new(e)))?; + + let has_ipv6 = config.ipv6_gateway.is_some(); + + let luid = handle.luid().to_owned(); + + let setup_task = async move { + log::debug!("Waiting for tunnel IP interfaces to arrive"); + talpid_windows::net::wait_for_interfaces(luid, true, has_ipv6) + .await + .map_err(|e| BoxedError::new(TunnelError::SetupIpInterfaces(e)))?; + log::debug!("Waiting for tunnel IP interfaces: Done"); + + if let Err(error) = talpid_tunnel::network_interface::initialize_interfaces(luid, None) + { + log::error!( + "{}", + error.display_chain_with_msg("Failed to set tunnel interface metric"), + ); + } + + Ok(()) + }; + + tokio::spawn(async move { + let _ = setup_done_tx.send(setup_task.await).await; + }); + + let interface_name = handle.name(); + + Ok(WgGoTunnel(WgGoTunnelState { + interface_name: interface_name.to_owned(), + tunnel_handle: handle, + _logging_context: logging_context, + _socket_update_cb: socket_update_cb, + #[cfg(daita)] + config: config.clone(), + })) + } + + // Callback to be used to rebind the tunnel sockets when the default route changes + #[cfg(target_os = "windows")] + fn default_route_changed_callback( + event_type: talpid_routing::EventType<'_>, + _family: talpid_windows::net::AddressFamily, + ) { + use talpid_routing::EventType::*; + match event_type { + // if there is no new default route, or if the route was removed, update the bind + Updated(_) | Removed => wireguard_go_rs::update_bind(), + // ignore interface updates that don't affect the interface to use + UpdatedDetails(_) => (), + } + } + + #[cfg(unix)] fn get_tunnel( tun_provider: Arc<Mutex<TunProvider>>, config: &Config, @@ -451,15 +548,14 @@ impl Tunnel for WgGoTunnel { } async fn get_tunnel_stats(&self) -> Result<StatsMap> { - tokio::task::block_in_place(|| { - self.as_state() - .tunnel_handle - .get_config(|cstr| { - Stats::parse_config_str(cstr.to_str().expect("Go strings are always UTF-8")) - }) - .ok_or(TunnelError::GetConfigError)? - .map_err(|error| TunnelError::StatsError(BoxedError::new(error))) - }) + // NOTE: wireguard-go might perform blocking I/O, but it's most likely not a problem + self.as_state() + .tunnel_handle + .get_config(|cstr| { + Stats::parse_config_str(cstr.to_str().expect("Go strings are always UTF-8")) + }) + .ok_or(TunnelError::GetConfigError)? + .map_err(|error| TunnelError::StatsError(BoxedError::new(error))) } fn set_config( diff --git a/talpid-wireguard/src/wireguard_nt/mod.rs b/talpid-wireguard/src/wireguard_nt/mod.rs index 9243425cde..fb4dfcbb22 100644 --- a/talpid-wireguard/src/wireguard_nt/mod.rs +++ b/talpid-wireguard/src/wireguard_nt/mod.rs @@ -1104,7 +1104,10 @@ impl Tunnel for WgNtTunnel { } #[cfg(daita)] - fn start_daita(&mut self) -> std::result::Result<(), crate::TunnelError> { + fn start_daita( + &mut self, + _: talpid_tunnel_config_client::DaitaSettings, + ) -> std::result::Result<(), crate::TunnelError> { self.spawn_machinist().map_err(|error| { log::error!( "{}", diff --git a/wireguard-go-rs/Cargo.toml b/wireguard-go-rs/Cargo.toml index cfaef554cc..f7572ab142 100644 --- a/wireguard-go-rs/Cargo.toml +++ b/wireguard-go-rs/Cargo.toml @@ -7,14 +7,17 @@ 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 +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] maybenot-ffi = "2.0.1" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52.0", features = ["Win32_Networking", "Win32_NetworkManagement", "Win32_NetworkManagement_Ndis", "Win32_Networking_WinSock"] } diff --git a/wireguard-go-rs/build.rs b/wireguard-go-rs/build.rs index 1148d4010e..f13f034116 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, @@ -9,32 +11,35 @@ use std::{ use anyhow::{anyhow, bail, Context}; fn main() -> anyhow::Result<()> { - let target_os = env::var("CARGO_CFG_TARGET_OS").context("Missing 'CARGO_CFG_TARGET_OS")?; - // Mark "daita" as a conditional configuration flag println!("cargo::rustc-check-cfg=cfg(daita)"); + // Enable the DAITA (rust) feature flag + println!(r#"cargo::rustc-cfg=daita"#); + // Rerun build-script if libwg (or wireguard-go) is changed 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)?, - "android" => build_android_dynamic_lib(true)?, - // building wireguard-go-rs for windows is not implemented - _ => {} + let out_dir = env::var("OUT_DIR").context("Missing OUT_DIR")?; + match target_os()? { + Os::Windows => build_windows_dynamic_lib(&out_dir)?, + Os::Linux => build_linux_static_lib(&out_dir)?, + Os::Macos => build_macos_static_lib(&out_dir)?, + Os::Android => build_android_dynamic_lib(&out_dir)?, } Ok(()) } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] enum Os { + Windows, Macos, Linux, + Android, } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] enum Arch { Amd64, Arm64, @@ -61,110 +66,382 @@ impl AndroidTarget { } } -fn host_os() -> anyhow::Result<Os> { +const fn host_os() -> 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") { - Ok(Os::Linux) + const HOST: Os = if cfg!(target_os = "windows") { + Os::Windows + } else if cfg!(target_os = "linux") { + Os::Linux } else if cfg!(target_os = "macos") { - Ok(Os::Macos) + Os::Macos } else { - bail!("Unsupported host OS") + panic!("Unsupported host OS") + }; + HOST +} + +fn target_os() -> anyhow::Result<Os> { + let target_os = env::var("CARGO_CFG_TARGET_OS").context("Missing 'CARGO_CFG_TARGET_OS")?; + match target_os.as_str() { + "windows" => Ok(Os::Windows), + "linux" => Ok(Os::Linux), + "macos" => Ok(Os::Macos), + "android" => Ok(Os::Android), + _ => bail!("Unsupported target os: {target_os}"), } } -fn host_arch() -> anyhow::Result<Arch> { - if cfg!(target_arch = "x86_64") { - Ok(Arch::Amd64) +const fn host_arch() -> Arch { + const ARCH: Arch = if cfg!(target_arch = "x86_64") { + Arch::Amd64 } else if cfg!(target_arch = "aarch64") { - Ok(Arch::Arm64) + Arch::Arm64 } else { - bail!("Unsupported host architecture") - } + panic!("Unsupported host architecture") + }; + ARCH } -/// Compile libwg as a static library and place it in `OUT_DIR`. -fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { - let out_dir = env::var("OUT_DIR").context("Missing OUT_DIR")?; +fn target_arch() -> anyhow::Result<Arch> { let target_arch = env::var("CARGO_CFG_TARGET_ARCH").context("Missing 'CARGO_CFG_TARGET_ARCH")?; - - let target_arch = match target_arch.as_str() { - "x86_64" => Arch::Amd64, - "aarch64" => Arch::Arm64, + match target_arch.as_str() { + "x86_64" => Ok(Arch::Amd64), + "aarch64" => Ok(Arch::Arm64), _ => bail!("Unsupported architecture: {target_arch}"), - }; + } +} + +/// Compile libwg and maybenot and place them in the target dir relative to `OUT_DIR`. +fn build_windows_dynamic_lib(out_dir: &str) -> anyhow::Result<()> { + let target_dir = Path::new(out_dir) + .ancestors() + .nth(3) + .context("Failed to find target dir")?; + build_shared_maybenot_lib(target_dir).context("Failed to build maybenot")?; + + let dll_path = target_dir.join("libwg.dll"); + let mut go_build = Command::new("go"); + go_build + .env("CGO_ENABLED", "1") + .current_dir("./libwg") + .args(["build", "-v"]) + .arg("-o") + .arg(&dll_path) + .args(["--tags", "daita"]) + // Build DLL + .args(["-buildmode", "c-shared"]) + // Needed for linking against maybenot-ffi + .env("CGO_LDFLAGS", format!("-L{}", target_dir.to_str().unwrap())) + .env("GOOS", "windows"); + + let target_arch = target_arch()?; + // We explicitly use zig for compiling libwg. Any MinGW-compatible toolchain should work. + match target_arch { + Arch::Amd64 => { + go_build.env("CC", "zig cc -target x86_64-windows"); + go_build.env("GOARCH", "amd64"); + } + Arch::Arm64 => { + go_build.env("CC", "zig cc -target aarch64-windows"); + go_build.env("GOARCH", "arm64"); + } + } + + generate_windows_lib(target_arch, target_dir)?; + + exec(go_build)?; + + println!("cargo::rustc-link-search={}", target_dir.to_str().unwrap()); + println!("cargo::rustc-link-lib=dylib=libwg"); + Ok(()) +} + +/// Compile libwg and place it in `OUT_DIR`. +fn build_linux_static_lib(out_dir: &str) -> anyhow::Result<()> { let out_file = format!("{out_dir}/libwg.a"); let mut go_build = Command::new("go"); go_build + .env("CGO_ENABLED", "1") + .current_dir("./libwg") .args(["build", "-v", "-o", &out_file]) + .args(["--tags", "daita"]) + // Build static lib .args(["-buildmode", "c-archive"]) - .args(if daita { &["--tags", "daita"][..] } else { &[] }) - .env("CGO_ENABLED", "1") - .current_dir("./libwg"); + .env("GOOS", "linux"); - // are we cross compiling? - let cross_compiling = host_os()? != target_os || host_arch()? != target_arch; + let target_arch = target_arch()?; + match target_arch { + Arch::Amd64 => go_build.env("GOARCH", "amd64"), + Arch::Arm64 => go_build.env("GOARCH", "arm64"), + }; + + if is_cross_compiling()? { + match target_arch { + Arch::Arm64 => go_build.env("CC", "aarch64-linux-gnu-gcc"), + Arch::Amd64 => bail!("cross-compiling to linux x86_64 is not implemented"), + }; + } + + 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"); + Ok(()) +} + +/// Compile libwg and place it in `OUT_DIR`. +fn build_macos_static_lib(out_dir: &str) -> anyhow::Result<()> { + let out_file = format!("{out_dir}/libwg.a"); + let mut go_build = Command::new("go"); + go_build + .env("CGO_ENABLED", "1") + .current_dir("./libwg") + .args(["build", "-v", "-o", &out_file]) + .args(["--tags", "daita"]) + // Build static lib + .args(["-buildmode", "c-archive"]) + .env("GOOS", "darwin"); + + let target_arch = target_arch()?; match target_arch { Arch::Amd64 => go_build.env("GOARCH", "amd64"), Arch::Arm64 => go_build.env("GOARCH", "arm64"), }; - match target_os { - Os::Linux => { - go_build.env("GOOS", "linux"); + if is_cross_compiling()? { + let sdkroot = env::var("SDKROOT").context("Missing 'SDKROOT'")?; + + let c_arch = match target_arch { + Arch::Amd64 => "x86_64", + Arch::Arm64 => "arm64", + }; + + let xcrun_output = exec(Command::new("xcrun").args(["-sdk", &sdkroot, "--find", "clang"]))?; + go_build.env("CC", xcrun_output); + + let cflags = format!("-isysroot {sdkroot} -arch {c_arch} -I{sdkroot}/usr/include"); + go_build.env("CFLAGS", cflags); + go_build.env("CGO_CFLAGS", format!("-isysroot {sdkroot} -arch {c_arch}")); + go_build.env("CGO_LDFLAGS", format!("-isysroot {sdkroot} -arch {c_arch}")); + go_build.env("LD_LIBRARY_PATH", format!("{sdkroot}/usr/lib")); + } + + exec(go_build)?; - if cross_compiling { - match target_arch { - Arch::Arm64 => go_build.env("CC", "aarch64-linux-gnu-gcc"), - Arch::Amd64 => bail!("cross-compiling to linux x86_64 is not implemented"), - }; + println!("cargo::rustc-link-search={out_dir}"); + println!("cargo::rustc-link-lib=static=wg"); + + Ok(()) +} + +/// Return whether compiling for an architecture or OS other than the host +fn is_cross_compiling() -> anyhow::Result<bool> { + Ok(host_os() != target_os()? || host_arch() != target_arch()?) +} + +// 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_category = env::var("PROFILE").context("Missing 'PROFILE'")?; + let profile = match profile_category.as_str() { + "release" => "release", + _ => "dev", + }; + + 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 let Some(stripped) = path_str.strip_prefix(r"\\?\") { + tmp_build_dir = PathBuf::from(stripped); + } + + 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(["--profile", profile]) + .args(["--target", &target_triple]); + + exec(build_command)?; + + let artifacts_dir = tmp_build_dir.join(target_triple).join(profile_category); + + // Copy library to desired target dir + for (src_filename, dest_filename) in [ + ("maybenot_ffi.dll", "maybenot_ffi.dll"), + ("maybenot_ffi.dll.lib", "maybenot.lib"), + ] { + let src = artifacts_dir.join(src_filename); + let dest = out_dir.as_ref().join(dest_filename); + fs::copy(&src, &dest).with_context(|| format!("Failed to copy {src_filename}",))?; + } + + Ok(()) +} + +/// Generate a library for the exported functions. Required for load-time linking. +/// This requires `msbuild.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") +} + +/// Find the correct `lib.exe` for this host and the target arch. +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(4) + .context("Unexpected msbuild.exe path")?; + + // This pattern can be found by browsing `C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\<MSVC-version>\bin\<host>` + let lib_exe_host = match host_arch() { + Arch::Amd64 => "Hostx64", + Arch::Arm64 => "Hostarm64", + }; + + // This pattern can be found by browsing `C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\<MSVC-version>\bin\<host>\<arch>` + let lib_exe_target = match target_arch()? { + Arch::Amd64 => "x64", + Arch::Arm64 => "arm64", + }; + + let lib_exe_pattern = format!( + "{host}/{target}/lib.exe", + host = lib_exe_host, + target = lib_exe_target, + ); + let path_is_lib_exe = |file: &Path| file.ends_with(&lib_exe_pattern); + + 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)? { + return Ok(Some(result)); } } - Os::Macos => { - go_build.env("GOOS", "darwin"); + if condition(&path) { + return Ok(Some(path.to_owned())); + } + } + Ok(None) +} - if cross_compiling { - let sdkroot = env::var("SDKROOT").context("Missing 'SDKROOT'")?; +/// 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") +} - let c_arch = match target_arch { - Arch::Amd64 => "x86_64", - Arch::Arm64 => "arm64", - }; +/// Generate lib from export +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 xcrun_output = - exec(Command::new("xcrun").args(["-sdk", &sdkroot, "--find", "clang"]))?; - go_build.env("CC", xcrun_output); + let lib_exe = find_lib_exe()?; - let cflags = format!("-isysroot {sdkroot} -arch {c_arch} -I{sdkroot}/usr/include"); - go_build.env("CFLAGS", cflags); - go_build.env("CGO_CFLAGS", format!("-isysroot {sdkroot} -arch {c_arch}")); - go_build.env("CGO_LDFLAGS", format!("-isysroot {sdkroot} -arch {c_arch}")); - go_build.env("LD_LIBRARY_PATH", format!("{sdkroot}/usr/lib")); - } + 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(go_build)?; + exec(lib_exe)?; - // make sure to link to the resulting binary - println!("cargo::rustc-link-search={out_dir}"); - println!("cargo::rustc-link-lib=static=wg"); + Ok(()) +} - // if daita is enabled, also enable the corresponding rust feature flag - if daita { - println!(r#"cargo::rustc-cfg=daita"#); +/// 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")?; + + for path in &[ + "./libwg/libwg.go", + "./libwg/libwg_windows.go", + "./libwg/libwg_daita.go", + ] { + for export in gather_exports(path).context("Failed to find 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<()> { - let out_dir = env::var("OUT_DIR").context("Missing OUT_DIR")?; +fn build_android_dynamic_lib(out_dir: &str) -> anyhow::Result<()> { let target_triple = env::var("TARGET").context("Missing 'TARGET'")?; let target = AndroidTarget::from_str(&target_triple)?; @@ -214,10 +491,8 @@ fn build_android_dynamic_lib(daita: bool) -> anyhow::Result<()> { println!("cargo::rustc-link-search={}", android_output_path.display()); println!("cargo::rustc-link-lib=dylib=wg"); - // If daita is enabled, also enable the corresponding rust feature flag - if daita { - println!(r#"cargo::rustc-cfg=daita"#); - } + // Enable the DAITA (rust) feature flag + println!(r#"cargo::rustc-cfg=daita"#); Ok(()) } diff --git a/wireguard-go-rs/libwg/README.md b/wireguard-go-rs/libwg/README.md index 39ad48e3e0..e5b96928f7 100644 --- a/wireguard-go-rs/libwg/README.md +++ b/wireguard-go-rs/libwg/README.md @@ -7,6 +7,7 @@ It currently offers support for the following platforms: - Linux - macOS - Android +- Windows # Organization @@ -16,6 +17,8 @@ It currently offers support for the following platforms: `libwg_android.go` has code specifically for Android. +`libwg_windows.go` has code specifically for Windows. + # Usage Call `wgTurnOn` to create and activate a tunnel. The prototype is different on different platforms, see the code for details. diff --git a/wireguard-go-rs/libwg/libwg.go b/wireguard-go-rs/libwg/libwg.go index 5dcc9141b2..599234cc2e 100644 --- a/wireguard-go-rs/libwg/libwg.go +++ b/wireguard-go-rs/libwg/libwg.go @@ -1,7 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 * * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. - * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2025 Mullvad VPN AB. All Rights Reserved. */ package main diff --git a/wireguard-go-rs/libwg/libwg_android.go b/wireguard-go-rs/libwg/libwg_android.go index caca9b04d0..f34085b80f 100644 --- a/wireguard-go-rs/libwg/libwg_android.go +++ b/wireguard-go-rs/libwg/libwg_android.go @@ -1,7 +1,10 @@ +//go:build android +// +build android + /* SPDX-License-Identifier: Apache-2.0 * * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. - * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2025 Mullvad VPN AB. All Rights Reserved. */ package main diff --git a/wireguard-go-rs/libwg/libwg_daita.go b/wireguard-go-rs/libwg/libwg_daita.go index b73be376a3..3904912bed 100644 --- a/wireguard-go-rs/libwg/libwg_daita.go +++ b/wireguard-go-rs/libwg/libwg_daita.go @@ -3,7 +3,7 @@ /* SPDX-License-Identifier: Apache-2.0 * - * Copyright (C) 2024 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2025 Mullvad VPN AB. All Rights Reserved. */ package main diff --git a/wireguard-go-rs/libwg/libwg_default.go b/wireguard-go-rs/libwg/libwg_default.go index 263c231a61..3d0e74c168 100644 --- a/wireguard-go-rs/libwg/libwg_default.go +++ b/wireguard-go-rs/libwg/libwg_default.go @@ -5,7 +5,7 @@ /* SPDX-License-Identifier: Apache-2.0 * * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. - * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2025 Mullvad VPN AB. All Rights Reserved. */ package main diff --git a/wireguard-go-rs/libwg/libwg_windows.go b/wireguard-go-rs/libwg/libwg_windows.go new file mode 100644 index 0000000000..3d02209d27 --- /dev/null +++ b/wireguard-go-rs/libwg/libwg_windows.go @@ -0,0 +1,129 @@ +//go:build windows +// +build windows + +/* SPDX-License-Identifier: Apache-2.0 + * + * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. + * Copyright (C) 2025 Mullvad VPN AB. All Rights Reserved. + */ + +package main + +// #include <stdlib.h> +// #include <stdint.h> +// #include <string.h> +import "C" + +import ( + "bufio" + "strings" + "unsafe" + + "golang.org/x/sys/windows" + + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun" + + "github.com/mullvad/mullvadvpn-app/wireguard/libwg/logging" + "github.com/mullvad/mullvadvpn-app/wireguard/libwg/tunnelcontainer" +) + +// Redefined here because otherwise the compiler doesn't realize it's a type alias for a type that's safe to export. +// Taken from the contained logging package. +type LogSink = unsafe.Pointer +type LogContext = C.uint64_t + +//export wgTurnOn +func wgTurnOn(cIfaceName *C.char, cIfaceNameOut *C.char, cIfaceNameOutSize C.size_t, cLuidOut *C.uint64_t, mtu C.uint16_t, cSettings *C.char, logSink LogSink, logContext LogContext) C.int32_t { + logger := logging.NewLogger(logSink, logging.LogContext(logContext)) + + if cIfaceName == nil { + logger.Errorf("cIfaceName is null\n") + return ERROR_GENERAL_FAILURE + } + + if cSettings == nil { + logger.Errorf("cSettings is null\n") + return ERROR_GENERAL_FAILURE + } + + settings := C.GoString(cSettings) + ifaceName := C.GoString(cIfaceName) + + // {AFE43773-E1F8-4EBB-8536-576AB86AFE9A} + networkId := windows.GUID{ + Data1: 0xafe43773, + Data2: 0xe1f8, + Data3: 0x4ebb, + Data4: [8]byte{0x85, 0x36, 0x57, 0x6a, 0xb8, 0x6a, 0xfe, 0x9a}, + } + + tun.WintunTunnelType = "Mullvad" + + wintun, err := tun.CreateTUNWithRequestedGUID(ifaceName, &networkId, int(mtu)) + if err != nil { + logger.Errorf("Failed to create tunnel\n") + logger.Errorf("%s\n", err) + return ERROR_INTERMITTENT_FAILURE + } + + nativeTun := wintun.(*tun.NativeTun) + + actualInterfaceName, err := nativeTun.Name() + if err != nil { + nativeTun.Close() + logger.Errorf("Failed to determine name of wintun adapter\n") + return ERROR_GENERAL_FAILURE + } + if actualInterfaceName != ifaceName { + // WireGuard picked a different name for the adapter than the one we expected. + // This indicates there is already an adapter with the name we intended to use. + logger.Verbosef("Failed to create adapter with specific name\n") + } + + device := device.NewDevice(wintun, conn.NewDefaultBind(), logger) + + setError := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings))) + if setError != nil { + logger.Errorf("Failed to set device configuration\n") + logger.Errorf("%s\n", setError) + device.Close() + return ERROR_GENERAL_FAILURE + } + + device.Up() + + context := tunnelcontainer.Context{ + Device: device, + Logger: logger, + } + + handle, err := tunnels.Insert(context) + if err != nil { + logger.Errorf("%s\n", err) + device.Close() + return ERROR_GENERAL_FAILURE + } + + if cIfaceNameOut != nil { + if int(cIfaceNameOutSize) <= len(actualInterfaceName) { + logger.Errorf("Interface name buffer too small\n") + device.Close() + return ERROR_GENERAL_FAILURE + } + C.strcpy(cIfaceNameOut, C.CString(actualInterfaceName)) + } + if cLuidOut != nil { + *cLuidOut = C.uint64_t(nativeTun.LUID()) + } + + return C.int32_t(handle) +} + +//export wgUpdateBind +func wgUpdateBind() { + tunnels.ForEach(func(tunnel tunnelcontainer.Context) { + tunnel.Device.BindUpdate() + }) +} diff --git a/wireguard-go-rs/libwg/logging/logging.go b/wireguard-go-rs/libwg/logging/logging.go index a6782ec39a..eb8bed3c9f 100644 --- a/wireguard-go-rs/libwg/logging/logging.go +++ b/wireguard-go-rs/libwg/logging/logging.go @@ -1,7 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 * * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. - * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2025 Mullvad VPN AB. All Rights Reserved. */ package logging diff --git a/wireguard-go-rs/libwg/tunnelcontainer/tunnelcontainer.go b/wireguard-go-rs/libwg/tunnelcontainer/tunnelcontainer.go index 79eacc2a17..63c931bf08 100644 --- a/wireguard-go-rs/libwg/tunnelcontainer/tunnelcontainer.go +++ b/wireguard-go-rs/libwg/tunnelcontainer/tunnelcontainer.go @@ -1,7 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 * * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. - * Copyright (C) 2020 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2025 Mullvad VPN AB. All Rights Reserved. */ package tunnelcontainer diff --git a/wireguard-go-rs/src/lib.rs b/wireguard-go-rs/src/lib.rs index 3e75506f61..a98c9e056a 100644 --- a/wireguard-go-rs/src/lib.rs +++ b/wireguard-go-rs/src/lib.rs @@ -6,18 +6,21 @@ //! //! The [`Tunnel`] type provides a safe Rust wrapper around the C FFI. -#![cfg(unix)] - -use core::{ - ffi::{c_char, CStr}, - mem::{ManuallyDrop, MaybeUninit}, - slice, -}; +use core::ffi::{c_char, CStr}; +use core::mem::ManuallyDrop; +#[cfg(target_os = "windows")] +use core::mem::MaybeUninit; +use core::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; @@ -27,13 +30,18 @@ pub type LoggingCallback = unsafe extern "system" fn(level: WgLogLevel, msg: *const c_char, context: LoggingContext); // Make symbols from maybenot-ffi visible to wireguard-go -#[cfg(daita)] +#[cfg(all(daita, any(target_os = "linux", target_os = "macos")))] use maybenot_ffi as _; /// A wireguard-go tunnel 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 +81,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 +105,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 +159,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. /// @@ -236,23 +302,11 @@ impl Drop for Tunnel { } } -/// Check whether `machines` contains a valid, LF-separated maybenot machines. Return an error -/// otherwise. -pub fn validate_maybenot_machines(machines: &CStr) -> Result<(), Error> { - use maybenot_ffi::MaybenotResult; - - let mut framework = MaybeUninit::uninit(); - // SAFETY: `machines` is a null-terminated string, and `&mut framework` is a valid pointer - let result = - unsafe { maybenot_ffi::maybenot_start(machines.as_ptr(), 0.0, 0.0, &mut framework) }; - - if result as u32 == MaybenotResult::Ok as u32 { - // SAFETY: `maybenot_start` succeeded, so `framework` points to a valid framework - unsafe { maybenot_ffi::maybenot_stop(framework.assume_init()) }; - Ok(()) - } else { - Err(Error::Other) - } +/// Rebind WireGuard endpoint sockets. When the default interface changes, this needs to be called +/// so that the UDP socket can be rebound to use the new interface +#[cfg(target_os = "windows")] +pub fn update_bind() { + unsafe { ffi::wgUpdateBind() } } fn result_from_code(code: i32) -> Result<(), Error> { @@ -276,7 +330,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 +342,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. /// @@ -362,5 +439,9 @@ mod ffi { /// Get the file descriptor of the tunnel IPv6 socket. #[cfg(target_os = "android")] pub fn wgGetSocketV6(handle: i32) -> Fd; + + /// Rebind endpoint sockets + #[cfg(target_os = "windows")] + pub fn wgUpdateBind(); } } |
