diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-09-03 15:02:04 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-09-03 15:02:04 +0200 |
| commit | 82ef8c8fb924351172d2baf93b77023e6a408a90 (patch) | |
| tree | 659b309c532c2085113c6bc27aac3fe478bcb2d5 | |
| parent | 5ba4fb290a2cb737147be093c82198500f3beaeb (diff) | |
| parent | 4cafc03d485e5c5809a559a20fbe8d6f73cebad0 (diff) | |
| download | mullvadvpn-82ef8c8fb924351172d2baf93b77023e6a408a90.tar.xz mullvadvpn-82ef8c8fb924351172d2baf93b77023e6a408a90.zip | |
Merge branch 'win-device-diag-logging'
21 files changed, 583 insertions, 122 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c1da73931..955b6ed7c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ Line wrap the file at 100 chars. Th - Add helpful warnings when clearing account history. This helps users not lose their account numbers. +#### Windows +- Add additional logging for tunnel devices and split tunneling to problem reports. + ### Security #### Windows - Block traffic to exit node from non-Mullvad processes. This fixes a leak where traffic could be diff --git a/Cargo.lock b/Cargo.lock index 13214926ba..eeda37cecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -985,6 +985,27 @@ dependencies = [ ] [[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5588,9 +5609,11 @@ dependencies = [ name = "talpid-core" version = "0.0.0" dependencies = [ + "anyhow", "async-trait", "bitflags 2.9.0", "chrono", + "csv", "duct", "either", "futures", @@ -5745,6 +5768,7 @@ version = "0.0.0" dependencies = [ "rs-release", "talpid-dbus", + "talpid-windows", "windows-sys 0.52.0", ] diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 5712135f9d..f7a22ee346 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -14,6 +14,7 @@ workspace = true boringtun = ["talpid-wireguard/boringtun"] [dependencies] +anyhow = { workspace = true } chrono = { workspace = true, features = ["clock"] } thiserror = { workspace = true } futures = { workspace = true } @@ -67,11 +68,13 @@ talpid-net = { path = "../talpid-net" } [target.'cfg(windows)'.dependencies] bitflags = "2.6" +csv = "1.3.1" widestring = "1.0" winreg = { version = "0.51", features = ["transactions"] } memoffset = "0.6" once_cell = { workspace = true } windows-service = "0.6.0" +serde = { workspace = true, features = ["derive"] } talpid-windows = { path = "../talpid-windows" } wmi = "0.14.0" diff --git a/talpid-core/src/dns/windows/netsh.rs b/talpid-core/src/dns/windows/netsh.rs index fe5a135e87..2f8a4d77d1 100644 --- a/talpid-core/src/dns/windows/netsh.rs +++ b/talpid-core/src/dns/windows/netsh.rs @@ -2,22 +2,20 @@ use crate::dns::{DnsMonitorT, ResolvedDnsConfig}; use std::{ - ffi::OsString, io::{self, Write}, net::IpAddr, - os::windows::prelude::{AsRawHandle, OsStringExt}, - path::PathBuf, + os::windows::prelude::AsRawHandle, process::{Child, Command, ExitStatus, Stdio}, time::Duration, }; use talpid_types::{ErrorExt, net::IpVersion}; -use talpid_windows::net::{index_from_luid, luid_from_alias}; +use talpid_windows::{ + env::get_system_dir, + net::{index_from_luid, luid_from_alias}, +}; use windows_sys::Win32::{ - Foundation::{MAX_PATH, WAIT_OBJECT_0, WAIT_TIMEOUT}, - System::{ - SystemInformation::GetSystemDirectoryW, - Threading::{INFINITE, WaitForSingleObject}, - }, + Foundation::{WAIT_OBJECT_0, WAIT_TIMEOUT}, + System::Threading::{INFINITE, WaitForSingleObject}, }; const NETSH_TIMEOUT: Duration = Duration::from_secs(10); @@ -213,14 +211,3 @@ fn create_netsh_flush_command(interface_index: u32, ip_version: IpVersion) -> St "interface {interface_type} set dnsservers name={interface_index} source=static address=none validate=no\r\n" ) } - -fn get_system_dir() -> io::Result<PathBuf> { - let mut sysdir = [0u16; MAX_PATH as usize + 1]; - let len = unsafe { GetSystemDirectoryW(sysdir.as_mut_ptr(), (sysdir.len() - 1) as u32) }; - if len == 0 { - return Err(io::Error::last_os_error()); - } - Ok(PathBuf::from(OsString::from_wide( - &sysdir[0..(len as usize)], - ))) -} diff --git a/talpid-core/src/firewall/windows/winfw/sys.rs b/talpid-core/src/firewall/windows/winfw/sys.rs index c320181d75..40bb61599c 100644 --- a/talpid-core/src/firewall/windows/winfw/sys.rs +++ b/talpid-core/src/firewall/windows/winfw/sys.rs @@ -1,10 +1,8 @@ //! Data types and thin wrappers around WinFW C FFI. use std::ffi::{CStr, c_char, c_void}; -use std::io; -use std::ptr; - -use windows_sys::Win32::Globalization::{CP_ACP, MultiByteToWideChar}; +use talpid_windows::string::multibyte_to_wide; +use windows_sys::Win32::Globalization::CP_ACP; use super::{Error, WideCString}; @@ -207,73 +205,3 @@ pub extern "system" fn log_sink( .build(), ); } - -/// Convert `mb_string`, with the given character encoding `codepage`, to a UTF-16 string. -pub fn multibyte_to_wide(mb_string: &CStr, codepage: u32) -> Result<Vec<u16>, io::Error> { - if mb_string.is_empty() { - return Ok(vec![]); - } - - // SAFETY: `mb_string` is null-terminated and valid. - let wc_size = unsafe { - MultiByteToWideChar( - codepage, - 0, - mb_string.as_ptr() as *const u8, - -1, - ptr::null_mut(), - 0, - ) - }; - - if wc_size == 0 { - return Err(io::Error::last_os_error()); - } - - let mut wc_buffer = vec![0u16; usize::try_from(wc_size).unwrap()]; - - // SAFETY: `wc_buffer` can contain up to `wc_size` characters, including a null - // terminator. - let chars_written = unsafe { - MultiByteToWideChar( - codepage, - 0, - mb_string.as_ptr() as *const u8, - -1, - wc_buffer.as_mut_ptr(), - wc_size, - ) - }; - - if chars_written == 0 { - return Err(io::Error::last_os_error()); - } - - wc_buffer.truncate(usize::try_from(chars_written - 1).unwrap()); - - Ok(wc_buffer) -} - -#[cfg(test)] -mod test { - use super::*; - use windows_sys::Win32::Globalization::CP_UTF8; - - #[test] - fn test_multibyte_to_wide() { - // € = 0x20AC in UTF-16 - let converted = multibyte_to_wide(c"€€", CP_UTF8); - const EXPECTED: &[u16] = &[0x20AC, 0x20AC]; - assert!( - matches!(converted.as_deref(), Ok(EXPECTED)), - "expected Ok({EXPECTED:?}), got {converted:?}", - ); - - // boundary case - let converted = multibyte_to_wide(c"", CP_UTF8); - assert!( - matches!(converted.as_deref(), Ok([])), - "unexpected result {converted:?}" - ); - } -} diff --git a/talpid-core/src/logging/diag.rs b/talpid-core/src/logging/diag.rs new file mode 100644 index 0000000000..419c0b7619 --- /dev/null +++ b/talpid-core/src/logging/diag.rs @@ -0,0 +1,337 @@ +//! Additional logging that may be useful + +/// Additional logging for Windows +#[cfg(target_os = "windows")] +pub mod windows { + use anyhow::Context; + use anyhow::bail; + use std::ffi::CStr; + use std::ffi::OsString; + use std::fmt::Write; + use std::os::windows::ffi::OsStringExt; + use std::path::Path; + use talpid_windows::env::get_system_dir; + use talpid_windows::string::multibyte_to_wide; + use tokio::io::AsyncWriteExt; + use tokio::process::Command; + use windows_sys::Win32::Globalization::CP_ACP; + + /// Keywords used to filter output + const KEYWORDS: &[&str] = &[ + "wireguard", + "wintun", + "tunnel", + "mullvad", + "split-tunnel", + "split tunnel", + ]; + + /// Dump logs about tunnel devices and relevant drivers + /// + /// Currently, this will log the output of `pnputil` and `driverquery`, with filtering. + pub async fn log_device_info(log_dir: &Path) -> anyhow::Result<()> { + use crate::logging::rotate_log; + use tokio::{fs::File, io::BufWriter}; + + const TIMESTAMP_FMT: &str = "%Y-%m-%d %H:%M:%S"; + + let log_path = log_dir.join("device.log"); + + tokio::task::block_in_place(|| rotate_log(&log_path)).context("Failed to rotate log")?; + + let logger = File::options() + .write(true) + .create(true) + .truncate(true) + .open(log_path) + .await + .context("Failed to open device log")?; + + let mut logger = BufWriter::new(logger); + + // Log the current time + logger + .write_all( + format!( + "Log time: {}\n\n", + chrono::Local::now().format(TIMESTAMP_FMT) + ) + .as_bytes(), + ) + .await?; + + async fn run_cmd_and_write_logs( + logger: &mut BufWriter<File>, + cmd: &mut Command, + parse_output: impl FnOnce(String) -> anyhow::Result<String>, + ) -> anyhow::Result<()> { + let logs = run_cmd(cmd).await.and_then(parse_output); + logger.write_all(format_logs(cmd, logs)?.as_bytes()).await?; + Ok(()) + } + + run_cmd_and_write_logs(&mut logger, &mut driverquery_cmd()?, parse_driverquery).await?; + run_cmd_and_write_logs(&mut logger, &mut pnputil_cmd()?, parse_pnputil).await?; + run_cmd_and_write_logs( + &mut logger, + &mut pnputil_problem_cmd()?, + parse_pnputil_problem, + ) + .await?; + + let _ = logger.flush().await; + + Ok(()) + } + + /// Run `cmd` and collect its output + async fn run_cmd(cmd: &mut Command) -> anyhow::Result<String> { + let out = cmd.output().await.context("Failed to run driverquery")?; + + if !out.status.success() { + bail!("driverquery failed: {:?}", out.status.code()); + } + + parse_raw_cmd_output(&out.stdout) + } + + fn format_logs(cmd: &Command, output: anyhow::Result<String>) -> anyhow::Result<String> { + let mut buf = String::new(); + + writeln!(&mut buf, "{} (filtered)", format_command(cmd)?)?; + writeln!(&mut buf, "--------")?; + + match output { + Ok(out) => { + writeln!(&mut buf, "{out}")?; + } + Err(err) => { + writeln!(&mut buf, "The command failed due to an error: {err}")?; + } + } + writeln!(&mut buf, "--------")?; + + Ok(buf) + } + + /// Partial CSV records for `driverquery /FO csv ...` + #[derive(serde::Deserialize, serde::Serialize)] + struct DriverQueryRecords { + #[serde(rename = "Module Name")] + module_name: String, + #[serde(rename = "Display Name")] + display_name: String, + #[serde(rename = "Description")] + description: String, + #[serde(rename = "Driver Type")] + driver_type: String, + #[serde(rename = "Start Mode")] + start_mode: String, + #[serde(rename = "State")] + state: String, + #[serde(rename = "Status")] + status: String, + #[serde(rename = "Path")] + path: String, + } + + fn parse_driverquery(out_s: String) -> anyhow::Result<String> { + parse_csv::<DriverQueryRecords>(out_s.as_bytes(), driverquery_filter) + } + + fn driverquery_filter(records: &DriverQueryRecords) -> bool { + string_contains_keyword(&records.module_name) + || string_contains_keyword(&records.display_name) + || string_contains_keyword(&records.description) + || string_contains_keyword(&records.path) + } + + /// Partial CSV records for `pnputil /format csv ...` + #[derive(serde::Deserialize, serde::Serialize)] + #[serde(rename_all = "PascalCase")] + struct PnputilRecords { + instance_id: String, + device_description: String, + status: String, + problem_code: String, + problem_status: String, + driver_name: String, + } + + fn parse_pnputil(out_s: String) -> anyhow::Result<String> { + parse_csv::<PnputilRecords>(out_s.as_bytes(), pnputil_filter) + } + + fn parse_pnputil_problem(out_s: String) -> anyhow::Result<String> { + // In this case, we keep all entries + parse_csv::<PnputilRecords>(out_s.as_bytes(), |_| true) + } + + fn pnputil_filter(records: &PnputilRecords) -> bool { + string_contains_keyword(&records.instance_id) + || string_contains_keyword(&records.device_description) + || string_contains_keyword(&records.driver_name) + } + + /// Return whether `s` contains one of the keywords in [KEYWORDS]. + /// This is case-insensitive. + fn string_contains_keyword(s: &str) -> bool { + KEYWORDS + .iter() + .any(|word| s.to_ascii_lowercase().contains(&word.to_ascii_lowercase())) + } + + fn parse_raw_cmd_output(bytes: &[u8]) -> anyhow::Result<String> { + // Convert from current codepage to UTF8 + // Seems not entirely correct, but probably good enough + let mut bytes = bytes.to_vec(); + bytes.push(0); + let bytes_cstr = CStr::from_bytes_until_nul(&bytes).unwrap(); + + let str = multibyte_to_wide(bytes_cstr, CP_ACP).context("Invalid pnputil output")?; + let out_s = OsString::from_wide(&str); + Ok(out_s.to_string_lossy().into_owned()) + } + + fn parse_csv<RecordType: serde::de::DeserializeOwned + serde::Serialize>( + data: &[u8], + filter_fn: impl Fn(&RecordType) -> bool, + ) -> anyhow::Result<String> { + let mut csv = csv::Reader::from_reader(data); + + let mut buf = vec![]; + let mut out = csv::Writer::from_writer(&mut buf); + + csv.deserialize() + .filter_map(|record_result| record_result.ok()) + .try_for_each(|record: RecordType| { + if !filter_fn(&record) { + return Ok(()); + } + out.serialize(record) + .context("Failed to serialize csv record") + })?; + + drop(out); + + Ok(String::from_utf8_lossy(&buf).into_owned()) + } + + fn driverquery_cmd() -> anyhow::Result<Command> { + let path = get_system_dir()?.join("driverquery.exe"); + let mut driver_query = Command::new(path); + driver_query.args(["/FO", "csv", "/V"]); + Ok(driver_query) + } + + fn pnputil_cmd() -> anyhow::Result<Command> { + let path = get_system_dir()?.join("pnputil.exe"); + let mut pnputil = Command::new(path); + // Enumerate network devices + pnputil.args([ + "/enum-devices", + "/class", + "{4d36e972-e325-11ce-bfc1-08002be10318}", + ]); + pnputil.args(["/format", "csv"]); + Ok(pnputil) + } + + fn pnputil_problem_cmd() -> anyhow::Result<Command> { + let path = get_system_dir()?.join("pnputil.exe"); + let mut pnputil = Command::new(path); + // Enumerate devices with issues + pnputil.args(["/enum-devices", "/problem"]); + pnputil.args(["/format", "csv"]); + Ok(pnputil) + } + + fn format_command(cmd: &Command) -> anyhow::Result<String> { + let mut s = String::new(); + + let prog = Path::new(cmd.as_std().get_program()) + .file_name() + .context("Missing command filename")? + .display(); + write!(&mut s, r#"{prog}"#)?; + + for arg in cmd.as_std().get_args() { + write!(&mut s, r#" "{}""#, arg.display())?; + } + + Ok(s) + } + + #[cfg(test)] + mod test { + use super::*; + + /// Test whether driverquery output is filtered correctly + #[tokio::test] + async fn test_driverquery_output() { + let test_output_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src/logging/driverquery-out.testdata"); + + // Uncomment to generate new output + //tokio::fs::write( + // &test_output_path, + // driverquery_cmd().unwrap().output().await.unwrap().stdout, + //) + //.await + //.unwrap(); + + let my_output = std::fs::read(test_output_path).unwrap(); + let my_output = parse_raw_cmd_output(&my_output).unwrap(); + let parsed_driverquery_output = parse_driverquery(my_output).unwrap(); + let formatted_driverquery_log = + format_logs(&driverquery_cmd().unwrap(), Ok(parsed_driverquery_output)).unwrap(); + + insta::assert_snapshot!(formatted_driverquery_log); + } + + /// Test whether pnputil output is filtered correctly + #[tokio::test] + async fn test_pnputil_output() { + let test_output_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src/logging/pnputil-out.testdata"); + + // Uncomment to generate new output + //tokio::fs::write( + // &test_output_path, + // pnputil_cmd().unwrap().output().await.unwrap().stdout, + //) + //.await + //.unwrap(); + + let my_output = std::fs::read(test_output_path).unwrap(); + let my_output = parse_raw_cmd_output(&my_output).unwrap(); + let parsed_output = parse_pnputil(my_output).unwrap(); + let formatted_logs = format_logs(&pnputil_cmd().unwrap(), Ok(parsed_output)).unwrap(); + + insta::assert_snapshot!(formatted_logs); + } + + /// Test whether pnputil output is filtered correctly + #[tokio::test] + async fn test_pnputil_problem_output() { + let test_output_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src/logging/pnputil-problem-out.testdata"); + + // Uncomment to generate new output + //tokio::fs::write( + // &test_output_path, + // pnputil_problem_cmd().unwrap().output().await.unwrap().stdout, + //) + //.await + //.unwrap(); + + let my_output = std::fs::read(test_output_path).unwrap(); + let my_output = parse_raw_cmd_output(&my_output).unwrap(); + let parsed_output = parse_pnputil_problem(my_output).unwrap(); + let formatted_logs = + format_logs(&pnputil_problem_cmd().unwrap(), Ok(parsed_output)).unwrap(); + + insta::assert_snapshot!(formatted_logs); + } + } +} diff --git a/talpid-core/src/logging/driverquery-out.testdata b/talpid-core/src/logging/driverquery-out.testdata new file mode 100644 index 0000000000..7c2add3411 --- /dev/null +++ b/talpid-core/src/logging/driverquery-out.testdata @@ -0,0 +1,5 @@ +"Module Name","Display Name","Description","Driver Type","Start Mode","State","Status","Accept Stop","Accept Pause","Paged Pool(bytes)","Code(bytes)","BSS(bytes)","Link Date","Path","Init(bytes)" +"ACPI","Microsoft ACPI Driver","Microsoft ACPI Driver","Kernel ","Boot","Running","OK","TRUE","FALSE","184 320","466 944","0","","C:\WINDOWS\system32\drivers\ACPI.sys","24 576" +"mullvad-split-tunnel","Mullvad Split Tunnel Service","Mullvad Split Tunnel Service","Kernel ","Manual","Stopped","OK","FALSE","FALSE","4 096","53 248","0","2022-09-22 11:41:33","\??\C:\Program Files\Mullvad VPN\resources\mullvad-split-tunnel.sys","8 192" +"MullvadWireGuard","MullvadWireGuard","MullvadWireGuard","Kernel ","Manual","Stopped","OK","FALSE","FALSE","0","131 072","0","2024-06-04 14:21:55","C:\WINDOWS\system32\drivers\mullvad-wireguard.sys","12 288" +"NDIS","NDIS System Driver","NDIS System Driver","Kernel ","Boot","Running","OK","TRUE","FALSE","331 776","1 028 096","0","","C:\WINDOWS\system32\drivers\ndis.sys","16 384" diff --git a/talpid-core/src/logging/mod.rs b/talpid-core/src/logging/mod.rs index bd09c9dc03..016f3b2cc3 100644 --- a/talpid-core/src/logging/mod.rs +++ b/talpid-core/src/logging/mod.rs @@ -1,5 +1,7 @@ use std::{fs, io, path::Path}; +pub mod diag; + /// Unable to create new log file #[derive(thiserror::Error, Debug)] #[error("Unable to create new log file")] diff --git a/talpid-core/src/logging/pnputil-out.testdata b/talpid-core/src/logging/pnputil-out.testdata new file mode 100644 index 0000000000..995767fea1 --- /dev/null +++ b/talpid-core/src/logging/pnputil-out.testdata @@ -0,0 +1,12 @@ +InstanceId,DeviceDescription,ClassName,ClassGuid,ManufacturerName,Status,ProblemCode,ProblemStatus,DriverName,ExtensionDriverNames +"SWD\MSRRAS\MS_PPPOEMINIPORT","WAN Miniport (PPPOE)","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","netrasa.inf","" +"SWD\MSRRAS\MS_PPTPMINIPORT","WAN Miniport (PPTP)","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","netrasa.inf","" +"SWD\MSRRAS\MS_AGILEVPNMINIPORT","WAN Miniport (IKEv2)","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","netavpna.inf","" +"ROOT\KDNIC\0000","Microsoft Kernel Debug Network Adapter","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","kdnic.inf","" +"USB\VID_0BDA&PID_8153\001000001","Realtek USB GbE Family Controller","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Realtek","Disconnected","","","oem1.inf","" +"BTH\MS_BTHPAN\8&19b34d4&0&2","Bluetooth Device (Personal Area Network)","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","bthpan.inf","" +"ROOT\VMS_MP\0000","Hyper-V Virtual Ethernet Adapter #3","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","wvms_mp_windows.inf","" +"ROOT\WINTUN\0000","Wintun Userspace Tunnel","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","WireGuard LLC","Started","","","oem2.inf","" +"SWD\MSRRAS\MS_NDISWANIPV6","WAN Miniport (IPv6)","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","netrasa.inf","" +"SWD\MSRRAS\MS_L2TPMINIPORT","WAN Miniport (L2TP)","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","netrasa.inf","" +"ROOT\VMS_VSMP\0000","Hyper-V Virtual Switch Extension Adapter","Net","{4d36e972-e325-11ce-bfc1-08002be10318}","Microsoft","Started","","","wvms_mp_windows.inf","" diff --git a/talpid-core/src/logging/pnputil-problem-out.testdata b/talpid-core/src/logging/pnputil-problem-out.testdata new file mode 100644 index 0000000000..bf909c0c1d --- /dev/null +++ b/talpid-core/src/logging/pnputil-problem-out.testdata @@ -0,0 +1,2 @@ +InstanceId,DeviceDescription,ClassName,ClassGuid,ManufacturerName,Status,ProblemCode,ProblemStatus,DriverName,ExtensionDriverNames +"UEFI\RES_{redacted}\0","Camera Firmware","Firmware","{f2e7dd72-6468-4e36-b6f1-6488f42c1b52}","Realtek Semiconductor Corp.","Problem","10","0xC0210000","oem84.inf","" diff --git a/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__driverquery_output.snap b/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__driverquery_output.snap new file mode 100644 index 0000000000..578441f5af --- /dev/null +++ b/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__driverquery_output.snap @@ -0,0 +1,12 @@ +--- +source: talpid-core/src/logging/diag.rs +expression: formatted_driverquery_log +snapshot_kind: text +--- +driverquery.exe "/FO" "csv" "/V" (filtered) +-------- +Module Name,Display Name,Description,Driver Type,Start Mode,State,Status,Path +mullvad-split-tunnel,Mullvad Split Tunnel Service,Mullvad Split Tunnel Service,Kernel ,Manual,Stopped,OK,\??\C:\Program Files\Mullvad VPN\resources\mullvad-split-tunnel.sys +MullvadWireGuard,MullvadWireGuard,MullvadWireGuard,Kernel ,Manual,Stopped,OK,C:\WINDOWS\system32\drivers\mullvad-wireguard.sys + +-------- diff --git a/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__pnputil_output.snap b/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__pnputil_output.snap new file mode 100644 index 0000000000..176a1f74fe --- /dev/null +++ b/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__pnputil_output.snap @@ -0,0 +1,11 @@ +--- +source: talpid-core/src/logging/diag.rs +expression: formatted_logs +snapshot_kind: text +--- +pnputil.exe "/enum-devices" "/class" "{4d36e972-e325-11ce-bfc1-08002be10318}" "/format" "csv" (filtered) +-------- +InstanceId,DeviceDescription,Status,ProblemCode,ProblemStatus,DriverName +ROOT\WINTUN\0000,Wintun Userspace Tunnel,Started,,,oem2.inf + +-------- diff --git a/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__pnputil_problem_output.snap b/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__pnputil_problem_output.snap new file mode 100644 index 0000000000..3660589ebe --- /dev/null +++ b/talpid-core/src/logging/snapshots/talpid_core__logging__diag__windows__test__pnputil_problem_output.snap @@ -0,0 +1,11 @@ +--- +source: talpid-core/src/logging/diag.rs +expression: formatted_logs +snapshot_kind: text +--- +pnputil.exe "/enum-devices" "/problem" "/format" "csv" (filtered) +-------- +InstanceId,DeviceDescription,Status,ProblemCode,ProblemStatus,DriverName +UEFI\RES_{redacted}\0,Camera Firmware,Problem,10,0xC0210000,oem84.inf + +-------- diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 9c89b3fc3d..7ba803478c 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -249,6 +249,9 @@ impl ConnectingState { tokio::task::spawn_blocking(move || { let start = Instant::now(); + #[cfg(target_os = "windows")] + let runtime2 = runtime.clone(); + let args = TunnelArgs { runtime, resource_dir: &resource_dir, @@ -259,6 +262,19 @@ impl ConnectingState { route_manager, }; + #[cfg(target_os = "windows")] + async fn maybe_dump_device_logs(log_dir: Option<&Path>, error: &tunnel::Error) { + if error.get_tunnel_device_error().is_some() + && let Some(log_dir) = log_dir + { + log::debug!("Logging device info"); + if let Err(err) = crate::logging::diag::windows::log_device_info(log_dir).await + { + log::error!("Failed to dump device logs: {err}"); + } + } + } + let block_reason = match TunnelMonitor::start(&tunnel_parameters, &log_dir, args) { Ok(monitor) => { let reason = Self::wait_for_tunnel_monitor(monitor, retry_attempt); @@ -272,10 +288,18 @@ impl ConnectingState { "Retrying to connect after failing to start tunnel" ) ); + #[cfg(target_os = "windows")] + runtime2.block_on(async { + maybe_dump_device_logs(log_dir.as_deref(), &error).await; + }); None } Err(error) => { log::error!("{}", error.display_chain_with_msg("Failed to start tunnel")); + #[cfg(target_os = "windows")] + runtime2.block_on(async { + maybe_dump_device_logs(log_dir.as_deref(), &error).await; + }); Some(error.into()) } }; diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 99237506ee..0dd029cf86 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -357,14 +357,27 @@ impl TunnelStateMachine { let filtering_resolver = crate::resolver::start_resolver(Default::default()).await?; #[cfg(windows)] - let split_tunnel = split_tunnel::SplitTunnel::new( - runtime.clone(), - args.resource_dir.clone(), - args.command_tx.clone(), - volume_update_rx, - args.route_manager.clone(), - ) - .map_err(Error::InitSplitTunneling)?; + let split_tunnel = { + let result = split_tunnel::SplitTunnel::new( + runtime.clone(), + args.resource_dir.clone(), + args.command_tx.clone(), + volume_update_rx, + args.route_manager.clone(), + ) + .map_err(Error::InitSplitTunneling); + + if result.is_err() + && let Some(log_dir) = &args.log_dir + { + log::debug!("Logging device info"); + if let Err(err) = crate::logging::diag::windows::log_device_info(log_dir).await { + log::error!("Failed to dump device logs: {err}"); + } + } + + result? + }; #[cfg(target_os = "macos")] let split_tunnel = diff --git a/talpid-platform-metadata/Cargo.toml b/talpid-platform-metadata/Cargo.toml index 66db6c4285..281914e721 100644 --- a/talpid-platform-metadata/Cargo.toml +++ b/talpid-platform-metadata/Cargo.toml @@ -18,6 +18,9 @@ network-manager = ["talpid-dbus"] rs-release = "0.1.7" talpid-dbus = { path = "../talpid-dbus", optional = true } +[target.'cfg(target_os = "windows")'.dependencies] +talpid-windows = { path = "../talpid-windows" } + [target.'cfg(windows)'.dependencies.windows-sys] workspace = true features = [ diff --git a/talpid-platform-metadata/src/windows.rs b/talpid-platform-metadata/src/windows.rs index 9a23cc21c4..6cb5599ceb 100644 --- a/talpid-platform-metadata/src/windows.rs +++ b/talpid-platform-metadata/src/windows.rs @@ -1,23 +1,20 @@ use std::{ - ffi::{OsStr, OsString}, + ffi::OsStr, io, iter, mem::{self, MaybeUninit}, - os::{ - raw::c_void, - windows::ffi::{OsStrExt, OsStringExt}, - }, - path::PathBuf, + os::{raw::c_void, windows::ffi::OsStrExt}, ptr, }; +use talpid_windows::env::get_system_dir; use windows_sys::Win32::{ - Foundation::{MAX_PATH, NTSTATUS, STATUS_SUCCESS}, + Foundation::{NTSTATUS, STATUS_SUCCESS}, Storage::FileSystem::{ GetFileVersionInfoSizeW, GetFileVersionInfoW, VS_FFI_SIGNATURE, VS_FIXEDFILEINFO, VerQueryValueW, }, System::{ LibraryLoader::{GetModuleHandleW, GetProcAddress}, - SystemInformation::{GetSystemDirectoryW, OSVERSIONINFOEXW}, + SystemInformation::OSVERSIONINFOEXW, SystemServices::VER_NT_WORKSTATION, }, }; @@ -223,18 +220,6 @@ fn ntoskrnl_version() -> io::Result<(u32, u32, u32)> { Ok((major, minor, build)) } -fn get_system_dir() -> io::Result<PathBuf> { - let mut sysdir = [0u16; MAX_PATH as usize + 1]; - // SAFETY: `sysdir` points to a valid buffer - let len = unsafe { GetSystemDirectoryW(sysdir.as_mut_ptr(), (sysdir.len() - 1) as u32) }; - if len == 0 { - return Err(io::Error::last_os_error()); - } - Ok(PathBuf::from(OsString::from_wide( - &sysdir[0..(len as usize)], - ))) -} - /// Return a null-terminated UTF16 string fn to_wide(s: impl AsRef<OsStr>) -> Vec<u16> { s.as_ref().encode_wide().chain(iter::once(0u16)).collect() diff --git a/talpid-windows/src/env.rs b/talpid-windows/src/env.rs new file mode 100644 index 0000000000..1f49307822 --- /dev/null +++ b/talpid-windows/src/env.rs @@ -0,0 +1,18 @@ +use std::io; +use std::os::windows::ffi::OsStringExt; +use std::{ffi::OsString, path::PathBuf}; + +use windows_sys::Win32::{Foundation::MAX_PATH, System::SystemInformation::GetSystemDirectoryW}; + +/// Get the system directory path. This is typically `C:\Windows\System32`. +pub fn get_system_dir() -> io::Result<PathBuf> { + let mut sysdir = [0u16; MAX_PATH as usize + 1]; + // SAFETY: We have a valid buffer and length + let len = unsafe { GetSystemDirectoryW(sysdir.as_mut_ptr(), (sysdir.len() - 1) as u32) }; + if len == 0 { + return Err(io::Error::last_os_error()); + } + Ok(PathBuf::from(OsString::from_wide( + &sysdir[0..(len as usize)], + ))) +} diff --git a/talpid-windows/src/lib.rs b/talpid-windows/src/lib.rs index 92db769623..27ddd877c2 100644 --- a/talpid-windows/src/lib.rs +++ b/talpid-windows/src/lib.rs @@ -3,6 +3,9 @@ #![deny(missing_docs)] #![cfg(windows)] +/// Environment +pub mod env; + /// File system pub mod fs; @@ -17,3 +20,6 @@ pub mod sync; /// Processes pub mod process; + +/// String functions +pub mod string; diff --git a/talpid-windows/src/string.rs b/talpid-windows/src/string.rs new file mode 100644 index 0000000000..28172d88d6 --- /dev/null +++ b/talpid-windows/src/string.rs @@ -0,0 +1,74 @@ +use std::ffi::CStr; +use std::io; +use std::ptr; +use windows_sys::Win32::Globalization::MultiByteToWideChar; + +/// Convert `mb_string`, with the given character encoding `codepage`, to a UTF-16 string. +pub fn multibyte_to_wide(mb_string: &CStr, codepage: u32) -> Result<Vec<u16>, io::Error> { + if mb_string.is_empty() { + return Ok(vec![]); + } + + // SAFETY: `mb_string` is null-terminated and valid. + let wc_size = unsafe { + MultiByteToWideChar( + codepage, + 0, + mb_string.as_ptr() as *const u8, + -1, + ptr::null_mut(), + 0, + ) + }; + + if wc_size == 0 { + return Err(io::Error::last_os_error()); + } + + let mut wc_buffer = vec![0u16; usize::try_from(wc_size).unwrap()]; + + // SAFETY: `wc_buffer` can contain up to `wc_size` characters, including a null + // terminator. + let chars_written = unsafe { + MultiByteToWideChar( + codepage, + 0, + mb_string.as_ptr() as *const u8, + -1, + wc_buffer.as_mut_ptr(), + wc_size, + ) + }; + + if chars_written == 0 { + return Err(io::Error::last_os_error()); + } + + wc_buffer.truncate(usize::try_from(chars_written - 1).unwrap()); + + Ok(wc_buffer) +} + +#[cfg(test)] +mod test { + use super::*; + use windows_sys::Win32::Globalization::CP_UTF8; + + #[test] + fn test_multibyte_to_wide() { + // € = 0x20AC in UTF-16 + let converted = multibyte_to_wide(c"€€", CP_UTF8); + const EXPECTED: &[u16] = &[0x20AC, 0x20AC]; + assert!( + matches!(converted.as_deref(), Ok(EXPECTED)), + "expected Ok({EXPECTED:?}), got {converted:?}", + ); + + // boundary case + let converted = multibyte_to_wide(c"", CP_UTF8); + assert!( + matches!(converted.as_deref(), Ok([])), + "unexpected result {converted:?}" + ); + } +} diff --git a/test/Cargo.lock b/test/Cargo.lock index fe9974abb2..ef2cb663c1 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -3662,6 +3662,7 @@ name = "talpid-platform-metadata" version = "0.0.0" dependencies = [ "rs-release", + "talpid-windows", "windows-sys 0.52.0", ] |
