refactor(client/windows): de-dupe wintun.dll (#6020)

Closes #5977

Refactored some other stuff to make this work

Also removed a redundant impl of `ensure_dll` in a benchmark
This commit is contained in:
Reactor Scram
2024-07-25 09:28:35 -05:00
committed by GitHub
parent 59014a9622
commit 82b8de4c9c
29 changed files with 154 additions and 272 deletions

8
rust/Cargo.lock generated
View File

@@ -1078,7 +1078,6 @@ dependencies = [
"ip-packet",
"ip_network",
"itertools 0.13.0",
"known-folders",
"libc",
"os_info",
"phoenix-channel",
@@ -1095,8 +1094,6 @@ dependencies = [
"tracing",
"url",
"uuid",
"windows 0.57.0",
"wintun",
]
[[package]]
@@ -1790,9 +1787,11 @@ dependencies = [
"futures",
"ip-packet",
"ip_network",
"known-folders",
"libc",
"netlink-packet-core",
"netlink-packet-route",
"ring",
"rtnetlink",
"tokio",
"tracing",
@@ -1851,9 +1850,9 @@ dependencies = [
"chrono",
"clap",
"connlib-client-shared",
"connlib-shared",
"crash-handler",
"dirs",
"firezone-bin-shared",
"firezone-headless-client",
"futures",
"git-version",
@@ -1917,7 +1916,6 @@ dependencies = [
"nix 0.28.0",
"phoenix-channel",
"resolv-conf",
"ring",
"rtnetlink",
"sd-notify",
"secrecy",

View File

@@ -29,8 +29,10 @@ rtnetlink = { workspace = true }
libc = "0.2"
[target.'cfg(target_os = "windows")'.dependencies]
wintun = "0.4.0"
known-folders = "1.1.0"
ring = "0.17"
uuid = { version = "1.10.0", features = ["v4"] }
wintun = "0.4.0"
[target.'cfg(windows)'.dependencies.windows]
version = "0.57.0"

View File

@@ -40,15 +40,6 @@ mod platform {
use tun::Tun as _;
pub(crate) async fn perf() -> Result<()> {
// Install wintun so the test can run
let wintun_path = connlib_shared::windows::wintun_dll_path().unwrap();
tokio::fs::create_dir_all(wintun_path.parent().unwrap())
.await
.unwrap();
tokio::fs::write(&wintun_path, connlib_shared::windows::wintun_bytes())
.await
.unwrap();
const MTU: usize = 1_280;
const NUM_REQUESTS: u64 = 1_000;
const REQ_CODE: u8 = 42;

View File

@@ -1,5 +1,8 @@
mod tun_device_manager;
#[cfg(target_os = "windows")]
pub mod windows;
use clap::Args;
use tracing_log::LogTracer;
use tracing_subscriber::{
@@ -7,6 +10,21 @@ use tracing_subscriber::{
};
use url::Url;
/// Bundle ID / App ID that the client uses to distinguish itself from other programs on the system
///
/// e.g. In ProgramData and AppData we use this to name our subdirectories for configs and data,
/// and Windows may use it to track things like the MSI installer, notification titles,
/// deep link registration, etc.
///
/// This should be identical to the `tauri.bundle.identifier` over in `tauri.conf.json`,
/// but sometimes I need to use this before Tauri has booted up, or in a place where
/// getting the Tauri app handle would be awkward.
///
/// Luckily this is also the AppUserModelId that Windows uses to label notifications,
/// so if your dev system has Firezone installed by MSI, the notifications will look right.
/// <https://learn.microsoft.com/en-us/windows/configuration/find-the-application-user-model-id-of-an-installed-app>
pub const BUNDLE_ID: &str = "dev.firezone.client";
/// Mark for Firezone sockets to prevent routing loops on Linux.
pub const FIREZONE_MARK: u32 = 0xfd002021;

View File

@@ -27,18 +27,6 @@ mod tests {
.with_test_writer()
.try_init();
#[cfg(target_os = "windows")]
{
// Install wintun so the test can run
let wintun_path = connlib_shared::windows::wintun_dll_path().unwrap();
tokio::fs::create_dir_all(wintun_path.parent().unwrap())
.await
.unwrap();
tokio::fs::write(&wintun_path, connlib_shared::windows::wintun_bytes())
.await
.unwrap();
}
// Run these tests in series since they would fight over the tunnel interface
// if they ran concurrently
create_tun();

View File

@@ -1,14 +1,14 @@
use crate::windows::CREATE_NO_WINDOW;
use anyhow::{Context as _, Result};
use connlib_shared::{
windows::{CREATE_NO_WINDOW, TUNNEL_NAME},
DEFAULT_MTU,
};
use connlib_shared::DEFAULT_MTU;
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
use ring::digest;
use std::{
collections::HashSet,
io,
io::{self, Read as _},
net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6},
os::windows::process::CommandExt,
path::{Path, PathBuf},
process::{Command, Stdio},
str::FromStr,
sync::Arc,
@@ -27,8 +27,9 @@ use windows::Win32::{
};
use wintun::Adapter;
// Not sure how this and `TUNNEL_NAME` differ
const ADAPTER_NAME: &str = "Firezone";
// wintun automatically append " Tunnel" to this
pub(crate) const TUNNEL_NAME: &str = "Firezone";
/// The ring buffer size used for Wintun.
///
/// Must be a power of two within a certain range <https://docs.rs/wintun/latest/wintun/struct.Adapter.html#method.start_session>
@@ -211,16 +212,15 @@ impl Tun {
pub fn new() -> Result<Self> {
const TUNNEL_UUID: &str = "e9245bc1-b8c1-44ca-ab1d-c6aad4f13b9c";
// SAFETY: we're loading a DLL from disk and it has arbitrary C code in it.
// The Windows client, in `wintun_install` hashes the DLL at startup, before calling connlib, so it's unlikely for the DLL to be accidentally corrupted by the time we get here.
let path = connlib_shared::windows::wintun_dll_path()?;
let path = ensure_dll()?;
// SAFETY: we're loading a DLL from disk and it has arbitrary C code in it. There's no perfect way to prove it's safe.
let wintun = unsafe { wintun::load_from_path(path) }?;
// Create wintun adapter
let uuid = uuid::Uuid::from_str(TUNNEL_UUID)
.expect("static UUID should always parse correctly")
.as_u128();
let adapter = &Adapter::create(&wintun, ADAPTER_NAME, TUNNEL_NAME, Some(uuid))?;
let adapter = &Adapter::create(&wintun, TUNNEL_NAME, TUNNEL_NAME, Some(uuid))?;
let iface_idx = adapter.get_adapter_index()?;
set_iface_config(adapter.get_luid(), DEFAULT_MTU as u32)?;
@@ -394,3 +394,82 @@ fn set_iface_config(luid: wintun::NET_LUID_LH, mtu: u32) -> Result<()> {
}
Ok(())
}
/// Installs the DLL in %LOCALAPPDATA% and returns the DLL's absolute path
///
/// e.g. `C:\Users\User\AppData\Local\dev.firezone.client\data\wintun.dll`
/// Also verifies the SHA256 of the DLL on-disk with the expected bytes packed into the exe
fn ensure_dll() -> Result<PathBuf> {
let dll_bytes = wintun_bytes();
let path = wintun_dll_path().context("Can't compute wintun.dll path")?;
// The DLL path should always have a parent
let dir = path.parent().context("wintun.dll path invalid")?;
std::fs::create_dir_all(dir).context("Can't create dirs for wintun.dll")?;
tracing::debug!(?path, "wintun.dll path");
// This hash check is not meant to protect against attacks. It only lets us skip redundant disk writes, and it updates the DLL if needed.
// `tun_windows.rs` in connlib, and `elevation.rs`, rely on thia.
if dll_already_exists(&path, &dll_bytes) {
return Ok(path);
}
std::fs::write(&path, dll_bytes.bytes).context("Failed to write wintun.dll")?;
Ok(path)
}
fn dll_already_exists(path: &Path, dll_bytes: &DllBytes) -> bool {
let mut f = match std::fs::File::open(path) {
Err(_) => return false,
Ok(x) => x,
};
let actual_len = usize::try_from(f.metadata().unwrap().len()).unwrap();
let expected_len = dll_bytes.bytes.len();
// If the dll is 100 MB instead of 0.5 MB, this allows us to skip a 100 MB read
if actual_len != expected_len {
return false;
}
let mut buf = vec![0u8; expected_len];
if f.read_exact(&mut buf).is_err() {
return false;
}
let expected = ring::test::from_hex(dll_bytes.expected_sha256).unwrap();
let actual = digest::digest(&digest::SHA256, &buf);
expected == actual.as_ref()
}
/// Returns the absolute path for installing and loading `wintun.dll`
///
/// e.g. `C:\Users\User\AppData\Local\dev.firezone.client\data\wintun.dll`
fn wintun_dll_path() -> Result<PathBuf> {
let path = crate::windows::app_local_data_dir()?
.join("data")
.join("wintun.dll");
Ok(path)
}
struct DllBytes {
/// Bytes embedded in the client with `include_bytes`
pub bytes: &'static [u8],
/// Expected SHA256 hash
pub expected_sha256: &'static str,
}
#[cfg(target_arch = "x86_64")]
fn wintun_bytes() -> DllBytes {
DllBytes {
bytes: include_bytes!("../wintun/bin/amd64/wintun.dll"),
expected_sha256: "e5da8447dc2c320edc0fc52fa01885c103de8c118481f683643cacc3220dafce",
}
}
#[cfg(target_arch = "aarch64")]
fn wintun_bytes() -> DllBytes {
DllBytes {
bytes: include_bytes!("../wintun/bin/arm64/wintun.dll"),
expected_sha256: "f7ba89005544be9d85231a9e0d5f23b2d15b3311667e2dad0debd344918a3f80",
}
}

View File

@@ -0,0 +1,21 @@
use anyhow::{Context as _, Result};
use known_folders::{get_known_folder_path, KnownFolder};
use std::path::PathBuf;
/// Hides Powershell's console on Windows
///
/// <https://stackoverflow.com/questions/59692146/is-it-possible-to-use-the-standard-library-to-spawn-a-process-without-showing-th#60958956>
/// Also used for self-elevation
pub const CREATE_NO_WINDOW: u32 = 0x08000000;
/// Returns e.g. `C:/Users/User/AppData/Local/dev.firezone.client
///
/// This is where we can save config, logs, crash dumps, etc.
/// It's per-user and doesn't roam across different PCs in the same domain.
/// It's read-write for non-elevated processes.
pub fn app_local_data_dir() -> Result<PathBuf> {
let path = get_known_folder_path(KnownFolder::LocalAppData)
.context("Can't find %LOCALAPPDATA% dir")?
.join(crate::BUNDLE_ID);
Ok(path)
}

View File

@@ -41,17 +41,5 @@ tokio = { version = "1.38", features = ["macros", "rt"] }
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
swift-bridge = { workspace = true }
# Windows tunnel dependencies
[target.'cfg(target_os = "windows")'.dependencies]
wintun = "0.4.0"
known-folders = "1.1.0"
# Windows Win32 API
[target.'cfg(windows)'.dependencies.windows]
version = "0.57.0"
features = [
"Win32_Foundation",
]
[lints]
workspace = true

View File

@@ -52,12 +52,6 @@ pub enum ConnlibError {
#[error("failed packet translation")]
FailedTranslation,
#[cfg(target_os = "windows")]
#[error("Windows error: {0}")]
WindowsError(#[from] windows::core::Error),
#[cfg(target_os = "windows")]
#[error(transparent)]
Wintun(#[from] wintun::Error),
#[cfg(target_os = "windows")]
#[error("Can't compute path for wintun.dll")]
WintunDllPath,
#[cfg(target_os = "windows")]

View File

@@ -7,9 +7,6 @@ pub mod callbacks;
pub mod error;
pub mod messages;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(feature = "proptest")]
pub mod proptest;
@@ -24,21 +21,6 @@ use rand_core::OsRng;
pub type DomainName = domain::base::Name<Vec<u8>>;
/// Bundle ID / App ID that the client uses to distinguish itself from other programs on the system
///
/// e.g. In ProgramData and AppData we use this to name our subdirectories for configs and data,
/// and Windows may use it to track things like the MSI installer, notification titles,
/// deep link registration, etc.
///
/// This should be identical to the `tauri.bundle.identifier` over in `tauri.conf.json`,
/// but sometimes I need to use this before Tauri has booted up, or in a place where
/// getting the Tauri app handle would be awkward.
///
/// Luckily this is also the AppUserModelId that Windows uses to label notifications,
/// so if your dev system has Firezone installed by MSI, the notifications will look right.
/// <https://learn.microsoft.com/en-us/windows/configuration/find-the-application-user-model-id-of-an-installed-app>
pub const BUNDLE_ID: &str = "dev.firezone.client";
pub const DEFAULT_MTU: usize = 1280;
const LIB_NAME: &str = "connlib";

View File

@@ -1,44 +0,0 @@
//! Windows-specific things like the well-known appdata path, bundle ID, etc.
use crate::Error;
use known_folders::{get_known_folder_path, KnownFolder};
use std::path::PathBuf;
/// Hides Powershell's console on Windows
///
/// <https://stackoverflow.com/questions/59692146/is-it-possible-to-use-the-standard-library-to-spawn-a-process-without-showing-th#60958956>
/// Also used for self-elevation
pub const CREATE_NO_WINDOW: u32 = 0x08000000;
// wintun automatically append " Tunnel" to this
pub const TUNNEL_NAME: &str = "Firezone";
/// Returns e.g. `C:/Users/User/AppData/Local/dev.firezone.client
///
/// This is where we can save config, logs, crash dumps, etc.
/// It's per-user and doesn't roam across different PCs in the same domain.
/// It's read-write for non-elevated processes.
pub fn app_local_data_dir() -> Result<PathBuf, Error> {
let path = get_known_folder_path(KnownFolder::LocalAppData)
.ok_or(Error::CantFindLocalAppDataFolder)?
.join(crate::BUNDLE_ID);
Ok(path)
}
/// Returns the absolute path for installing and loading `wintun.dll`
///
/// e.g. `C:\Users\User\AppData\Local\dev.firezone.client\data\wintun.dll`
pub fn wintun_dll_path() -> Result<PathBuf, Error> {
let path = app_local_data_dir()?.join("data").join("wintun.dll");
Ok(path)
}
#[cfg(target_arch = "aarch64")]
pub fn wintun_bytes() -> &'static [u8] {
include_bytes!("../../../headless-client/src/windows/wintun/bin/arm64/wintun.dll")
}
#[cfg(target_arch = "x86_64")]
pub fn wintun_bytes() -> &'static [u8] {
include_bytes!("../../../headless-client/src/windows/wintun/bin/amd64/wintun.dll")
}

View File

@@ -18,8 +18,8 @@ atomicwrites = "0.4.3"
chrono = { workspace = true }
clap = { version = "4.5", features = ["derive", "env"] }
connlib-client-shared = { workspace = true }
connlib-shared = { workspace = true }
crash-handler = "0.6.2"
firezone-bin-shared = { workspace = true }
firezone-headless-client = { path = "../../headless-client" }
futures = { version = "0.3", default-features = false }
git-version = "0.3.9"

View File

@@ -3,7 +3,7 @@
use super::FZ_SCHEME;
use anyhow::{Context, Result};
use connlib_shared::BUNDLE_ID;
use firezone_bin_shared::BUNDLE_ID;
use secrecy::Secret;
use std::{io, path::Path, time::Duration};
use tokio::{io::AsyncReadExt, io::AsyncWriteExt, net::windows::named_pipe};

View File

@@ -185,7 +185,7 @@ pub(crate) fn run(
}
assert_eq!(
connlib_shared::BUNDLE_ID,
firezone_bin_shared::BUNDLE_ID,
app.handle().config().tauri.bundle.identifier,
"BUNDLE_ID should match bundle ID in tauri.conf.json"
);

View File

@@ -1,4 +1,5 @@
use anyhow::{Context as _, Result};
use firezone_bin_shared::BUNDLE_ID;
use tauri::api::notification::Notification;
pub(crate) async fn set_autostart(enabled: bool) -> Result<()> {
@@ -44,7 +45,7 @@ pub(crate) fn show_update_notification(
/// Show a notification in the bottom right of the screen
pub(crate) fn show_notification(title: &str, body: &str) -> Result<()> {
Notification::new(connlib_shared::BUNDLE_ID)
Notification::new(BUNDLE_ID)
.title(title)
.body(body)
.show()?;

View File

@@ -1,6 +1,6 @@
use super::{ControllerRequest, CtlrTx};
use anyhow::{Context, Result};
use connlib_shared::BUNDLE_ID;
use firezone_bin_shared::BUNDLE_ID;
#[allow(clippy::unused_async)]
pub(crate) async fn set_autostart(_enabled: bool) -> Result<()> {

View File

@@ -54,7 +54,6 @@ dirs = "5.0.1"
[target.'cfg(target_os = "windows")'.dependencies]
ipconfig = "0.3.2"
known-folders = "1.1.0"
ring = "0.17"
thiserror = { version = "1.0", default-features = false }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
windows-service = "0.7.0"

View File

@@ -14,7 +14,7 @@
//! <https://superuser.com/a/1752670>
use anyhow::{Context as _, Result};
use connlib_shared::windows::CREATE_NO_WINDOW;
use firezone_bin_shared::windows::CREATE_NO_WINDOW;
use std::{net::IpAddr, os::windows::process::CommandExt, path::Path, process::Command};
pub fn system_resolvers_for_gui() -> Result<Vec<IpAddr>> {

View File

@@ -1,5 +1,6 @@
use super::{Error, ServiceId};
use anyhow::{anyhow, Context as _, Result};
use firezone_bin_shared::BUNDLE_ID;
use std::{io::ErrorKind, os::unix::fs::PermissionsExt, path::PathBuf};
use tokio::net::{UnixListener, UnixStream};
@@ -88,9 +89,7 @@ impl Server {
/// Test sockets live in e.g. `/run/user/1000/dev.firezone.client/data/`
fn ipc_path(id: ServiceId) -> PathBuf {
match id {
ServiceId::Prod => PathBuf::from("/run")
.join(connlib_shared::BUNDLE_ID)
.join("ipc.sock"),
ServiceId::Prod => PathBuf::from("/run").join(BUNDLE_ID).join("ipc.sock"),
ServiceId::Test(id) => crate::known_dirs::runtime()
.expect("`known_dirs::runtime()` should always work")
.join(format!("ipc_test_{id}.sock")),

View File

@@ -1,6 +1,6 @@
use super::{Error, ServiceId};
use anyhow::{bail, Context as _, Result};
use connlib_shared::BUNDLE_ID;
use firezone_bin_shared::BUNDLE_ID;
use std::{ffi::c_void, io::ErrorKind, os::windows::io::AsRawHandle, time::Duration};
use tokio::net::windows::named_pipe;
use windows::Win32::{
@@ -49,7 +49,6 @@ impl Server {
/// This is async on Linux
#[allow(clippy::unused_async)]
pub(crate) async fn new(id: ServiceId) -> Result<Self> {
crate::platform::setup_before_connlib()?;
let pipe_path = ipc_path(id);
Ok(Self { pipe_path })
}

View File

@@ -1,4 +1,4 @@
use connlib_shared::BUNDLE_ID;
use firezone_bin_shared::BUNDLE_ID;
use std::path::PathBuf;
/// Path for IPC service config that either the IPC service or GUI can write

View File

@@ -1,4 +1,4 @@
use connlib_shared::BUNDLE_ID;
use firezone_bin_shared::{windows::app_local_data_dir, BUNDLE_ID};
use known_folders::{get_known_folder_path, KnownFolder};
use std::path::PathBuf;
@@ -13,7 +13,7 @@ use std::path::PathBuf;
pub fn ipc_service_config() -> Option<PathBuf> {
Some(
get_known_folder_path(KnownFolder::ProgramData)?
.join(connlib_shared::BUNDLE_ID)
.join(BUNDLE_ID)
.join("config"),
)
}
@@ -31,43 +31,26 @@ pub fn ipc_service_logs() -> Option<PathBuf> {
///
/// See connlib docs for details
pub fn logs() -> Option<PathBuf> {
Some(
connlib_shared::windows::app_local_data_dir()
.ok()?
.join("data")
.join("logs"),
)
Some(app_local_data_dir().ok()?.join("data").join("logs"))
}
/// e.g. `C:\Users\Alice\AppData\Local\dev.firezone.client\data`
///
/// Crash handler socket and other temp files go here
pub fn runtime() -> Option<PathBuf> {
Some(
connlib_shared::windows::app_local_data_dir()
.ok()?
.join("data"),
)
Some(app_local_data_dir().ok()?.join("data"))
}
/// e.g. `C:\Users\Alice\AppData\Local\dev.firezone.client\data`
///
/// Things like actor name go here
pub fn session() -> Option<PathBuf> {
Some(
connlib_shared::windows::app_local_data_dir()
.ok()?
.join("data"),
)
Some(app_local_data_dir().ok()?.join("data"))
}
/// e.g. `C:\Users\Alice\AppData\Local\dev.firezone.client\config`
///
/// See connlib docs for details
pub fn settings() -> Option<PathBuf> {
Some(
connlib_shared::windows::app_local_data_dir()
.ok()?
.join("config"),
)
Some(app_local_data_dir().ok()?.join("config"))
}

View File

@@ -2,7 +2,7 @@
use super::TOKEN_ENV_KEY;
use anyhow::{bail, Result};
use firezone_bin_shared::FIREZONE_MARK;
use firezone_bin_shared::{BUNDLE_ID, FIREZONE_MARK};
use nix::sys::socket::{setsockopt, sockopt};
use socket_factory::{TcpSocket, UdpSocket};
use std::{
@@ -29,9 +29,7 @@ pub(crate) fn udp_socket_factory(socket_addr: &SocketAddr) -> io::Result<UdpSock
}
pub(crate) fn default_token_path() -> PathBuf {
PathBuf::from("/etc")
.join(connlib_shared::BUNDLE_ID)
.join("token")
PathBuf::from("/etc").join(BUNDLE_ID).join("token")
}
pub(crate) fn check_token_permissions(path: &Path) -> Result<()> {
@@ -68,11 +66,3 @@ pub(crate) fn check_token_permissions(path: &Path) -> Result<()> {
pub(crate) fn notify_service_controller() -> Result<()> {
Ok(sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?)
}
/// Platform-specific setup needed for connlib
///
/// On Linux this does nothing
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn setup_before_connlib() -> Result<()> {
Ok(())
}

View File

@@ -164,7 +164,6 @@ pub fn run_only_headless_client() -> Result<()> {
// The name matches that in `ipc_service.rs`
let mut last_connlib_start_instant = Some(Instant::now());
platform::setup_before_connlib()?;
let args = ConnectArgs {
udp_socket_factory: Arc::new(crate::udp_socket_factory),
tcp_socket_factory: Arc::new(crate::tcp_socket_factory),

View File

@@ -10,9 +10,6 @@ use std::path::{Path, PathBuf};
pub(crate) use socket_factory::tcp as tcp_socket_factory;
pub(crate) use socket_factory::udp as udp_socket_factory;
#[path = "windows/wintun_install.rs"]
mod wintun_install;
// The return value is useful on Linux
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn check_token_permissions(_path: &Path) -> Result<()> {
@@ -32,8 +29,3 @@ pub(crate) fn default_token_path() -> std::path::PathBuf {
pub(crate) fn notify_service_controller() -> Result<()> {
Ok(())
}
pub(crate) fn setup_before_connlib() -> Result<()> {
wintun_install::ensure_dll()?;
Ok(())
}

View File

@@ -1,97 +0,0 @@
//! "Installs" wintun.dll at runtime by copying it into whatever folder the exe is in
use connlib_shared::windows::wintun_dll_path;
use ring::digest;
use std::{
fs,
io::{self, Read},
path::{Path, PathBuf},
};
struct DllBytes {
/// Bytes embedded in the client with `include_bytes`
bytes: &'static [u8],
/// Expected SHA256 hash
expected_sha256: &'static str,
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum Error {
#[error("Can't compute path where wintun.dll should be installed")]
CantComputeWintunPath,
#[error("create_dir_all failed")]
CreateDirAll,
#[error("Computed DLL path is invalid")]
DllPathInvalid,
#[error("permission denied")]
PermissionDenied,
#[error("write failed: `{0:?}`")]
WriteFailed(io::Error),
}
/// Installs the DLL in %LOCALAPPDATA% and returns the DLL's absolute path
///
/// e.g. `C:\Users\User\AppData\Local\dev.firezone.client\data\wintun.dll`
/// Also verifies the SHA256 of the DLL on-disk with the expected bytes packed into the exe
pub(crate) fn ensure_dll() -> Result<PathBuf, Error> {
let dll_bytes = get_dll_bytes();
let path = wintun_dll_path().map_err(|_| Error::CantComputeWintunPath)?;
// The DLL path should always have a parent
let dir = path.parent().ok_or(Error::DllPathInvalid)?;
std::fs::create_dir_all(dir).map_err(|_| Error::CreateDirAll)?;
tracing::debug!(?path, "wintun.dll path");
// This hash check is not meant to protect against attacks. It only lets us skip redundant disk writes, and it updates the DLL if needed.
// `tun_windows.rs` in connlib, and `elevation.rs`, rely on thia.
if !dll_already_exists(&path, &dll_bytes) {
fs::write(&path, dll_bytes.bytes).map_err(|e| {
#[allow(clippy::wildcard_enum_match_arm)]
match e.kind() {
io::ErrorKind::PermissionDenied => Error::PermissionDenied,
_ => Error::WriteFailed(e),
}
})?;
}
Ok(path)
}
fn dll_already_exists(path: &Path, dll_bytes: &DllBytes) -> bool {
let mut f = match fs::File::open(path) {
Err(_) => return false,
Ok(x) => x,
};
let actual_len = usize::try_from(f.metadata().unwrap().len()).unwrap();
let expected_len = dll_bytes.bytes.len();
// If the dll is 100 MB instead of 0.5 MB, this allows us to skip a 100 MB read
if actual_len != expected_len {
return false;
}
let mut buf = vec![0u8; expected_len];
if f.read_exact(&mut buf).is_err() {
return false;
}
let expected = ring::test::from_hex(dll_bytes.expected_sha256).unwrap();
let actual = digest::digest(&digest::SHA256, &buf);
expected == actual.as_ref()
}
#[cfg(target_arch = "x86_64")]
fn get_dll_bytes() -> DllBytes {
DllBytes {
bytes: include_bytes!("wintun/bin/amd64/wintun.dll"),
expected_sha256: "e5da8447dc2c320edc0fc52fa01885c103de8c118481f683643cacc3220dafce",
}
}
#[cfg(target_arch = "aarch64")]
fn get_dll_bytes() -> DllBytes {
DllBytes {
bytes: include_bytes!("wintun/bin/arm64/wintun.dll"),
expected_sha256: "f7ba89005544be9d85231a9e0d5f23b2d15b3311667e2dad0debd344918a3f80",
}
}