From 54d38ace078e98dd3a1b156db519b43803ed8e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 26 May 2025 09:22:01 +0200 Subject: [PATCH 1/2] Update dependences; small cleanups; now builds on NetBSD --- Cargo.lock | 54 +++++++-------- Cargo.toml | 16 ++--- src/bsd/ifconfig.rs | 7 +- src/bsd/nvlist.rs | 8 +-- src/bsd/route.rs | 2 +- src/host.rs | 4 +- src/lib.rs | 1 - src/utils.rs | 153 ++++++++++++++++++++++++++++------------- src/wgapi.rs | 2 +- src/wgapi_userspace.rs | 16 +---- 10 files changed, 150 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9430129..bec40b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,20 +52,20 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "autocfg" @@ -81,9 +81,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "byteorder" @@ -152,7 +152,7 @@ dependencies = [ [[package]] name = "defguard_wireguard_rs" -version = "0.7.2" +version = "0.7.3" dependencies = [ "base64", "env_logger", @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -202,9 +202,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -219,9 +219,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "jiff" -version = "0.2.5" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", @@ -232,9 +232,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.5" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", @@ -243,9 +243,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "log" @@ -345,9 +345,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", @@ -357,10 +357,10 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "paste" @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -491,9 +491,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2b182ef..d7e2ca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard_wireguard_rs" -version = "0.7.2" +version = "0.7.3" edition = "2021" rust-version = "1.80" description = "A unified multi-platform high-level API for managing WireGuard interfaces" @@ -22,17 +22,9 @@ x25519-dalek = { version = "2.0", features = ["getrandom", "static_secrets"] } env_logger = "0.11" serde_test = "1.0" -[target.'cfg(target_os = "freebsd")'.dependencies] +[target.'cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))'.dependencies] libc = { version = "0.2", default-features = false } -nix = { version = "0.29", features = ["ioctl", "socket"] } - -[target.'cfg(target_os = "macos")'.dependencies] -libc = { version = "0.2", default-features = false } -nix = { version = "0.29", features = ["ioctl", "socket"] } - -[target.'cfg(target_os = "netbsd")'.dependencies] -libc = { version = "0.2", default-features = false } -nix = { version = "0.29", features = ["ioctl", "socket"] } +nix = { version = "0.30", features = ["ioctl", "socket"] } [target.'cfg(target_os = "linux")'.dependencies] netlink-packet-core = "0.7" @@ -48,5 +40,7 @@ check_dependencies = [] serde = ["dep:serde"] [profile.release] +codegen-units = 1 +panic = "abort" lto = "thin" strip = "symbols" diff --git a/src/bsd/ifconfig.rs b/src/bsd/ifconfig.rs index c4a1490..d38fad4 100644 --- a/src/bsd/ifconfig.rs +++ b/src/bsd/ifconfig.rs @@ -70,11 +70,8 @@ type IfName = [u8; IF_NAMESIZE]; fn make_ifr_name(if_name: &str) -> IfName { let mut ifr_name = [0u8; IF_NAMESIZE]; - if_name - .bytes() - .take(IF_NAMESIZE - 1) - .enumerate() - .for_each(|(i, b)| ifr_name[i] = b); + let len = if_name.len().min(IF_NAMESIZE - 1); + ifr_name[..len].copy_from_slice(&if_name.as_bytes()[..len]); ifr_name } diff --git a/src/bsd/nvlist.rs b/src/bsd/nvlist.rs index ef2deb0..48be663 100644 --- a/src/bsd/nvlist.rs +++ b/src/bsd/nvlist.rs @@ -5,7 +5,7 @@ use std::{error::Error, ffi::CStr, fmt}; /// `NV_HEADER_SIZE` is for both: `nvlist_header` and `nvpair_header`. const NV_HEADER_SIZE: usize = 19; const NV_NAME_MAX: usize = 2048; -const NVLIST_HEADER_MAGIC: u8 = 0x6c; // 'l' +const NVLIST_HEADER_MAGIC: u8 = b'l'; const NVLIST_HEADER_VERSION: u8 = 0; // Public flags // Perform case-insensitive lookups of provided names. @@ -186,7 +186,7 @@ impl<'a> NvList<'a> { self.items.iter().find(|(n, _)| n == &name).map(|(_, v)| v) } - /// Get value as `bool`. + // Get value as `bool`. // pub fn get_bool(&self, name: &str) -> Option { // self.get(name).and_then(|value| match value { // NvValue::Bool(boolean) => Some(*boolean), @@ -202,7 +202,7 @@ impl<'a> NvList<'a> { }) } - /// Get value as `&str`. + // Get value as `&str`. // pub fn get_string(&self, name: &str) -> Option<&str> { // self.get(name).and_then(|value| match value { // NvValue::String(string) => Some(*string), @@ -242,7 +242,7 @@ impl<'a> NvList<'a> { self.items.push((name, NvValue::Number(number))); } - /// Append `String` value to the list. + // Append `String` value to the list. // pub fn append_string(&mut self, name: &'a str, string: &'a str) { // self.items.push((name, NvValue::String(string))); // } diff --git a/src/bsd/route.rs b/src/bsd/route.rs index e4da3ea..4981015 100644 --- a/src/bsd/route.rs +++ b/src/bsd/route.rs @@ -373,7 +373,7 @@ impl RtMessage { } let mut buf = [0u8; 256]; // FIXME: fixed buffer size - let len = read(socket.as_raw_fd(), &mut buf).map_err(IoError::ReadIo)?; + let len = read(socket.as_fd(), &mut buf).map_err(IoError::ReadIo)?; if len < size_of::() { return Err(IoError::Unpack); } diff --git a/src/host.rs b/src/host.rs index b20e749..d4243d1 100644 --- a/src/host.rs +++ b/src/host.rs @@ -314,9 +314,9 @@ impl Host { break; } } - return Err(io::Error::new(io::ErrorKind::Other, "error reading UAPI")); + return Err(io::Error::other("error reading UAPI")); } - _ => error!("Unknown UAPI keyword {}", keyword), + _ => error!("Unknown UAPI keyword {keyword}"), } } } diff --git a/src/lib.rs b/src/lib.rs index f5e2fe4..01eae30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,6 @@ mod wireguard_interface; extern crate log; use std::fmt; -#[cfg(not(target_os = "windows"))] use std::process::Output; #[cfg(feature = "serde")] diff --git a/src/utils.rs b/src/utils.rs index 2935381..b3fcdd2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,7 +10,7 @@ use std::{io::Write, process::Stdio}; #[cfg(not(target_os = "windows"))] use std::{net::IpAddr, process::Command}; -#[cfg(target_os = "freebsd")] +#[cfg(any(target_os = "freebsd", target_os = "netbsd"))] use crate::check_command_output_status; #[cfg(not(target_os = "windows"))] use crate::Peer; @@ -31,7 +31,10 @@ pub(crate) fn configure_dns( search_domains: &[&str], ) -> Result<(), WireguardInterfaceError> { // Build the resolvconf command - debug!("Starting DNS servers configuration for interface {ifname}, DNS: {dns:?}, search domains: {search_domains:?}"); + debug!( + "Starting DNS servers configuration for interface {ifname}, DNS: {dns:?}, search \ + domains: {search_domains:?}" + ); let mut cmd = Command::new("resolvconf"); let mut args = vec!["-a", ifname, "-m", "0"]; // Set the exclusive flag if no search domains are provided, @@ -44,7 +47,10 @@ pub(crate) fn configure_dns( match cmd.stdin(Stdio::piped()).spawn() { Ok(mut child) => { - debug!("Command resolvconf spawned successfully, proceeding with writing nameservers and search domains to its stdin"); + debug!( + "Command resolvconf spawned successfully, proceeding with writing nameservers \ + and search domains to its stdin" + ); if let Some(mut stdin) = child.stdin.take() { for entry in dns { debug!("Adding nameserver entry: {entry}"); @@ -62,12 +68,16 @@ pub(crate) fn configure_dns( debug!("DNS servers and search domains set successfully for interface {ifname}"); Ok(()) } else { - Err(WireguardInterfaceError::DnsError(format!("Failed to execute resolvconf command while setting DNS servers and search domains: {status}"))) + Err(WireguardInterfaceError::DnsError(format!( + "Failed to execute resolvconf \ + command while setting DNS servers and search domains: {status}" + ))) } } - Err(e) => { - Err(WireguardInterfaceError::DnsError(format!("Failed to execute resolvconf command while setting DNS servers and search domains: {e}"))) - } + Err(e) => Err(WireguardInterfaceError::DnsError(format!( + "Failed to execute resolvconf command \ + while setting DNS servers and search domains: {e}" + ))), } } @@ -101,7 +111,10 @@ pub(crate) fn configure_dns( dns: &[IpAddr], search_domains: &[&str], ) -> Result<(), WireguardInterfaceError> { - debug!("Configuring DNS servers and search domains, DNS: {dns:?}, search domains: {search_domains:?}"); + debug!( + "Configuring DNS servers and search domains, DNS: {dns:?}, search domains: \ + {search_domains:?}" + ); debug!("Setting DNS servers and search domains for all network services"); for service in network_services()? { @@ -138,13 +151,19 @@ pub(crate) fn configure_dns( let status = cmd.status()?; if !status.success() { - return Err(WireguardInterfaceError::DnsError(format!("Command `networksetup` failed while setting search domains for {service}: {status}"))); + return Err(WireguardInterfaceError::DnsError(format!( + "Command `networksetup` failed \ + while setting search domains for {service}: {status}" + ))); } debug!("Search domains set successfully for {service}"); } - debug!("The following DNS servers and search domains were set successfully: DNS: {dns:?}, search domains: {search_domains:?}"); + debug!( + "The following DNS servers and search domains were set successfully: DNS: {dns:?}, \ + search domains: {search_domains:?}" + ); Ok(()) } @@ -269,9 +288,13 @@ pub(crate) fn add_peer_routing( let mut gateway_v6 = Ok(None); for addr in &peer.allowed_ips { debug!("Processing route for allowed IP: {addr}, interface: {ifname}"); - // FIXME: currently it is impossible to add another default route, so use the hack from wg-quick for Darwin. + // FIXME: currently it is impossible to add another default route, so use the hack from + // wg-quick for Darwin. if addr.ip.is_unspecified() && addr.cidr == 0 { - debug!("Found following default route in the allowed IPs: {addr}, interface: {ifname}, proceeding with default route initial setup..."); + debug!( + "Found following default route in the allowed IPs: {addr}, interface: \ + {ifname}, proceeding with default route initial setup..." + ); let default1; let default2; if addr.ip.is_ipv4() { @@ -294,33 +317,45 @@ pub(crate) fn add_peer_routing( } match add_linked_route(&default1, ifname) { Ok(()) => debug!("Route to {default1} has been added for interface {ifname}"), - Err(err) => { - match err { - IoError::WriteIo(Errno::ENETUNREACH) => { - warn!("Failed to add default route {default1} for interface {ifname}: Network is unreachable. \ - This may happen if your interface's IP address is not the same IP version as the default gateway ({default1}) that was tried to be set, in this case this warning can be ignored. \ - Otherwise, there may be some other issues with your network configuration."); - } - _ => { - error!("Failed to add route to {default1} for interface {ifname}: {err}"); - } + Err(err) => match err { + IoError::WriteIo(Errno::ENETUNREACH) => { + warn!( + "Failed to add default route {default1} for interface \ + {ifname}: Network is unreachable. This may happen if your \ + interface's IP address is not the same IP version as the \ + default gateway ({default1}) that was tried to be set, in this \ + case this warning can be ignored. Otherwise, there may be some \ + other issues with your network configuration." + ); } - } + _ => { + error!( + "Failed to add route to {default1} for interface {ifname}: \ + {err}" + ); + } + }, } match add_linked_route(&default2, ifname) { Ok(()) => debug!("Route to {default2} has been added for interface {ifname}"), - Err(err) => { - match err { - IoError::WriteIo(Errno::ENETUNREACH) => { - warn!("Failed to add default route {default2} for interface {ifname}: Network is unreachable. \ - This may happen if your interface's IP address is not the same IP version as the default gateway ({default2}) that was tried to be set, in this case this warning can be ignored. \ - Otherwise, there may be some other issues with your network configuration."); - } - _ => { - error!("Failed to add route to {default2} for interface {ifname}: {err}"); - } + Err(err) => match err { + IoError::WriteIo(Errno::ENETUNREACH) => { + warn!( + "Failed to add default route {default2} for interface \ + {ifname}: Network is unreachable. This may happen if your \ + interface's IP address is not the same IP version as the \ + default gateway ({default2}) that was tried to be set, in this \ + case this warning can be ignored. Otherwise, there may be some \ + other issues with your network configuration." + ); } - } + _ => { + error!( + "Failed to add route to {default2} for interface {ifname}: \ + {err}" + ); + } + }, } } else { // Equivalent to `route -n add -inet[6] -interface `. @@ -354,54 +389,80 @@ pub(crate) fn add_peer_routing( } } if endpoint.is_ipv6() && default_route_v6 { - debug!("Endpoint is an IPv6 address and a default route (IPv6) is present in the alloweds IPs, proceeding with further configuration..."); + debug!( + "Endpoint is an IPv6 address and a default route (IPv6) is present in \ + the alloweds IPs, proceeding with further configuration..." + ); match gateway_v6 { Ok(Some(gateway)) => { - debug!("Default gateway for IPv4 has been found before: {gateway}, routing the traffic destined to {host} through it..."); + debug!( + "Default gateway for IPv4 has been found before: {gateway}, \ + routing the traffic destined to {host} through it..." + ); match add_gateway(&host, gateway, false) { Ok(()) => { debug!("Route to {host} has been added for gateway {gateway}"); } Err(err) => { - error!("Failed to add route to {host} for gateway {gateway}: {err}"); + error!( + "Failed to add route to {host} for gateway {gateway}: \ + {err}" + ); } - }; + } } Ok(None) => { - debug!("Default gateway for IPv6 has not been found, routing the traffic destined to {host} through localhost as a blackhole route..."); + debug!( + "Default gateway for IPv6 has not been found, routing the \ + traffic destined to {host} through localhost as a blackhole \ + route..." + ); match add_gateway(&host, localhost, true) { Ok(()) => debug!("Blackhole route to {host} has been added"), Err(err) => { error!("Failed to add blackhole route to {host}: {err}"); } - }; + } } Err(err) => { error!("Failed to get gateway for {host}: {err}"); } } } else if default_route_v4 { - debug!("Endpoint is an IPv4 address and a default route (IPv4) is present in the alloweds IPs, proceeding with further configuration..."); + debug!( + "Endpoint is an IPv4 address and a default route (IPv4) is present in \ + the alloweds IPs, proceeding with further configuration..." + ); match gateway_v4 { Ok(Some(gateway)) => { - debug!("Default gateway for IPv4 has been found before: {gateway}, routing the traffic destined to {host} through it..."); + debug!( + "Default gateway for IPv4 has been found before: {gateway}, \ + routing the traffic destined to {host} through it..." + ); match add_gateway(&host, gateway, false) { Ok(()) => { debug!("Added route to {host} for gateway {gateway}"); } Err(err) => { - error!("Failed to add route to {host} for gateway {gateway}: {err}"); + error!( + "Failed to add route to {host} for gateway {gateway}: \ + {err}" + ); } - }; + } } Ok(None) => { - debug!("Default gateway for IPv4 has not been found, routing the traffic destined to {host} through localhost as a blackhole route..."); + debug!( + "Default gateway for IPv4 has not been found, routing the \ + traffic destined to {host} through localhost as a blackhole \ + route..." + ); match add_gateway(&host, localhost, true) { Ok(()) => debug!("Blackhole route to {host} has been added"), Err(err) => { error!("Failed to add blackhole route to {host}: {err}"); } - }; + } } Err(err) => { error!("Failed to get gateway for {host}: {err}"); diff --git a/src/wgapi.rs b/src/wgapi.rs index d0b6fa2..7db4113 100644 --- a/src/wgapi.rs +++ b/src/wgapi.rs @@ -17,7 +17,7 @@ pub struct WGApi { pub(super) _api: PhantomData, } -impl WGApi { +impl WGApi { /// Create new instance of `WGApi`. pub fn new(ifname: String) -> Result { #[cfg(feature = "check_dependencies")] diff --git a/src/wgapi_userspace.rs b/src/wgapi_userspace.rs index aa021e6..fde11a8 100644 --- a/src/wgapi_userspace.rs +++ b/src/wgapi_userspace.rs @@ -1,7 +1,6 @@ use std::{ fs, io::{self, BufRead, BufReader, ErrorKind, Read, Write}, - marker::PhantomData, net::{IpAddr, Shutdown}, os::unix::net::UnixStream, process::Command, @@ -32,19 +31,6 @@ const USERSPACE_EXECUTABLE: &str = "wireguard-go"; /// We assume that `wireguard-go` executable is managed externally and available in `PATH`. /// Currently works on Unix platforms. impl WGApi { - /// Create new instance of `WireguardApiUserspace`. - /// - /// # Errors - /// Will return `WireguardInterfaceError` if `wireguard-go` can't be found. - pub fn new(ifname: String) -> Result { - #[cfg(feature = "check_dependencies")] - check_external_dependencies()?; - Ok(WGApi { - ifname, - _api: PhantomData, - }) - } - fn socket_path(&self) -> String { format!("/var/run/wireguard/{}.sock", self.ifname) } @@ -315,7 +301,7 @@ impl WireguardInterfaceApi for WGApi { debug!("Clearing DNS entries by applying an empty DNS list to all network services, interface {}", self.ifname); configure_dns(&[], &[])?; } - #[cfg(any(target_os = "linux", target_os = "freebsd"))] + #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] { debug!("Clearing DNS entries for interface {}", self.ifname); clear_dns(&self.ifname)?; From fe1672443fd7fa224175deaf877fc94230d50efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 15:29:12 +0200 Subject: [PATCH 2/2] Restore cfg --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 01eae30..f5e2fe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,7 @@ mod wireguard_interface; extern crate log; use std::fmt; +#[cfg(not(target_os = "windows"))] use std::process::Output; #[cfg(feature = "serde")]