summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-01-24 17:35:39 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-01-24 17:35:39 +0100
commit654de1cd2d3cdde6a3c26fe0cf5f26a4b0d1a89c (patch)
tree05a467738ba56f6c1affeefaaffddc41deba2121
parentff88a777e7c73dd1b6ea995df517d37bb26cc7e0 (diff)
parent0d5ba1a5b6ff2d3b8b37d36cd4997082a7ac5dcb (diff)
downloadmullvadvpn-654de1cd2d3cdde6a3c26fe0cf5f26a4b0d1a89c.tar.xz
mullvadvpn-654de1cd2d3cdde6a3c26fe0cf5f26a4b0d1a89c.zip
Merge branch 'win-daita-v2'
-rw-r--r--.github/workflows/clippy.yml11
-rw-r--r--.github/workflows/daemon.yml7
-rw-r--r--.github/workflows/desktop-e2e.yml2
-rw-r--r--.github/workflows/rust-unused-dependencies.yml12
-rw-r--r--.gitignore2
-rw-r--r--BuildInstructions.md9
-rw-r--r--CHANGELOG.md2
-rw-r--r--Cargo.lock1
-rwxr-xr-xbuild.sh2
-rw-r--r--desktop/packages/mullvad-vpn/tasks/distribution.js3
-rw-r--r--talpid-tunnel-config-client/src/lib.rs50
-rw-r--r--talpid-wireguard/Cargo.toml2
-rw-r--r--talpid-wireguard/build.rs7
-rw-r--r--talpid-wireguard/src/connectivity/mock.rs2
-rw-r--r--talpid-wireguard/src/ephemeral.rs19
-rw-r--r--talpid-wireguard/src/lib.rs51
-rw-r--r--talpid-wireguard/src/wireguard_go/mod.rs120
-rw-r--r--talpid-wireguard/src/wireguard_nt/mod.rs5
-rw-r--r--wireguard-go-rs/Cargo.toml7
-rw-r--r--wireguard-go-rs/build.rs419
-rw-r--r--wireguard-go-rs/libwg/README.md3
-rw-r--r--wireguard-go-rs/libwg/libwg.go2
-rw-r--r--wireguard-go-rs/libwg/libwg_android.go5
-rw-r--r--wireguard-go-rs/libwg/libwg_daita.go2
-rw-r--r--wireguard-go-rs/libwg/libwg_default.go2
-rw-r--r--wireguard-go-rs/libwg/libwg_windows.go129
-rw-r--r--wireguard-go-rs/libwg/logging/logging.go2
-rw-r--r--wireguard-go-rs/libwg/tunnelcontainer/tunnelcontainer.go2
-rw-r--r--wireguard-go-rs/src/lib.rs135
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",
]
diff --git a/build.sh b/build.sh
index 904ca3aabf..6d76dc017f 100755
--- a/build.sh
+++ b/build.sh
@@ -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();
}
}