diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6c80a5c8b..7228fc3f4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2073,6 +2073,7 @@ dependencies = [ "url", "uuid", "windows 0.52.0", + "windows-implement 0.52.0", "winreg 0.51.0", "wintun", "zip", @@ -5933,7 +5934,7 @@ dependencies = [ "unicode-segmentation", "uuid", "windows 0.39.0", - "windows-implement", + "windows-implement 0.39.0", "x11-dl", ] @@ -7324,7 +7325,7 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows 0.39.0", - "windows-implement", + "windows-implement 0.39.0", ] [[package]] @@ -7430,7 +7431,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" dependencies = [ - "windows-implement", + "windows-implement 0.39.0", "windows_aarch64_msvc 0.39.0", "windows_i686_gnu 0.39.0", "windows_i686_msvc 0.39.0", @@ -7464,6 +7465,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core 0.52.0", + "windows-implement 0.52.0", + "windows-interface", "windows-targets 0.52.0", ] @@ -7505,6 +7508,28 @@ dependencies = [ "windows-tokens", ] +[[package]] +name = "windows-implement" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "windows-interface" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + [[package]] name = "windows-metadata" version = "0.39.0" @@ -7876,7 +7901,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows 0.39.0", - "windows-implement", + "windows-implement 0.39.0", ] [[package]] diff --git a/rust/windows-client/src-tauri/Cargo.toml b/rust/windows-client/src-tauri/Cargo.toml index 619c72c07..0c96561b4 100755 --- a/rust/windows-client/src-tauri/Cargo.toml +++ b/rust/windows-client/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ url = { version = "2.5.0", features = ["serde"] } uuid = { version = "1.5.0", features = ["v4"] } tracing-panic = "0.1.1" zip = { version = "0.6.6", features = ["deflate", "time"], default-features = false } +windows-implement = "0.52.0" # These dependencies are locked behind `cfg(windows)` because they either can't compile at all on Linux, or they need native dependencies like glib that are difficult to get. Try not to add more here. @@ -49,10 +50,16 @@ wintun = "0.3.2" [target.'cfg(windows)'.dependencies.windows] version = "0.52.0" features = [ + # For implementing COM interfaces + "implement", "Win32_Foundation", - # Needed for deep_link module + # For listening for network change events + "Win32_Networking_NetworkListManager", + # For deep_link module "Win32_Security", - # Needed for deep_link module + # COM is needed to listen for network change events + "Win32_System_Com", + # For deep_link module "Win32_System_SystemServices", ] diff --git a/rust/windows-client/src-tauri/src/client.rs b/rust/windows-client/src-tauri/src/client.rs index 831b975b0..0fb8773c3 100644 --- a/rust/windows-client/src-tauri/src/client.rs +++ b/rust/windows-client/src-tauri/src/client.rs @@ -10,6 +10,7 @@ mod device_id; mod elevation; mod gui; mod logging; +mod network_changes; mod resolvers; mod settings; mod wintun_install; @@ -69,6 +70,7 @@ pub(crate) fn run() -> Result<()> { } Some(Cmd::DebugCrash) => debug_commands::crash(), Some(Cmd::DebugHostname) => debug_commands::hostname(), + Some(Cmd::DebugNetworkChanges) => debug_commands::network_changes(), Some(Cmd::DebugPipeServer) => debug_commands::pipe_server(), Some(Cmd::DebugWintun) => debug_commands::wintun(cli), // If we already tried to elevate ourselves, don't try again diff --git a/rust/windows-client/src-tauri/src/client/cli.rs b/rust/windows-client/src-tauri/src/client/cli.rs index da6fb0d6e..b9ee22729 100755 --- a/rust/windows-client/src-tauri/src/client/cli.rs +++ b/rust/windows-client/src-tauri/src/client/cli.rs @@ -14,6 +14,7 @@ pub enum CliCommands { Debug, DebugCrash, DebugHostname, + DebugNetworkChanges, DebugPipeServer, DebugWintun, Elevated, diff --git a/rust/windows-client/src-tauri/src/client/debug_commands.rs b/rust/windows-client/src-tauri/src/client/debug_commands.rs index e61cfbfc7..4a0fc5158 100644 --- a/rust/windows-client/src-tauri/src/client/debug_commands.rs +++ b/rust/windows-client/src-tauri/src/client/debug_commands.rs @@ -4,6 +4,7 @@ use crate::client::cli::Cli; use anyhow::Result; use tokio::runtime::Runtime; +use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_MULTITHREADED}; // TODO: In tauri-plugin-deep-link, this is the identifier in tauri.conf.json const PIPE_NAME: &str = "dev.firezone.client"; @@ -24,6 +25,28 @@ pub fn hostname() -> Result<()> { Ok(()) } +/// Listen for network change events from Windows +pub fn network_changes() -> Result<()> { + tracing_subscriber::fmt::init(); + + // Must be called for each thread that will do COM stuff + unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }?; + + { + let _listener = crate::client::network_changes::Listener::new()?; + println!("Listening for network events for 1 minute"); + std::thread::sleep(std::time::Duration::from_secs(60)); + } + + unsafe { + // Required, per CoInitializeEx docs + // Safety: Make sure all the COM objects are dropped before we call + // CoUninitialize or the program might segfault. + CoUninitialize(); + } + Ok(()) +} + pub fn open_deep_link(path: &url::Url) -> Result<()> { tracing_subscriber::fmt::init(); diff --git a/rust/windows-client/src-tauri/src/client/network_changes.rs b/rust/windows-client/src-tauri/src/client/network_changes.rs new file mode 100755 index 000000000..db77dce77 --- /dev/null +++ b/rust/windows-client/src-tauri/src/client/network_changes.rs @@ -0,0 +1,90 @@ +use windows::{ + core::{ComInterface, Result as WinResult, GUID}, + Win32::{ + Networking::NetworkListManager::{ + INetworkEvents, INetworkEvents_Impl, INetworkListManager, NetworkListManager, + NLM_CONNECTIVITY, NLM_NETWORK_PROPERTY_CHANGE, + }, + System::Com::{CoCreateInstance, IConnectionPoint, IConnectionPointContainer, CLSCTX_ALL}, + }, +}; + +pub(crate) struct Listener { + /// The cookie we get back from `Advise`. Can be None if the owner called `close` + advise_cookie: Option, + /// An IConnectionPoint is where we register our CallbackHandler + cxn_point: IConnectionPoint, +} + +impl Drop for Listener { + fn drop(&mut self) { + self.close().unwrap(); + } +} + +impl Listener { + /// Pre-req: CoInitializeEx must have been called on the calling thread to + /// initialize COM. + pub fn new() -> anyhow::Result { + // `windows-rs` automatically releases (de-refs) COM objects on Drop: + // https://github.com/microsoft/windows-rs/issues/2123#issuecomment-1293194755 + // https://github.com/microsoft/windows-rs/blob/cefdabd15e4a7a7f71b7a2d8b12d5dc148c99adb/crates/samples/windows/wmi/src/main.rs#L22 + let network_list_manager: INetworkListManager = + unsafe { CoCreateInstance(&NetworkListManager, None, CLSCTX_ALL) }?; + let cpc: IConnectionPointContainer = network_list_manager.cast()?; + let cxn_point = unsafe { cpc.FindConnectionPoint(&INetworkEvents::IID) }?; + let listener: INetworkEvents = CallbackHandler {}.into(); + // TODO: Make sure to call Unadvise later to avoid leaks + let advise_cookie = Some(unsafe { cxn_point.Advise(&listener) }?); + + Ok(Self { + advise_cookie, + cxn_point, + }) + } + + /// This is the same as Drop, but you can catch errors from it + /// Calling this multiple times is idempotent + fn close(&mut self) -> anyhow::Result<()> { + if let Some(advise_cookie) = self.advise_cookie.take() { + // SAFETY: I don't see any memory safety issues. + unsafe { self.cxn_point.Unadvise(advise_cookie) }?; + tracing::debug!("Unadvised"); + } + Ok(()) + } +} + +// https://kennykerr.ca/rust-getting-started/how-to-implement-com-interface.html +#[windows_implement::implement(INetworkEvents)] +struct CallbackHandler {} +impl INetworkEvents_Impl for CallbackHandler { + fn NetworkAdded(&self, networkid: &GUID) -> WinResult<()> { + // TODO: Send these events over a Tokio mpsc channel if we need them in the GUI + println!("NetworkAdded {networkid:?}"); + Ok(()) + } + + fn NetworkDeleted(&self, networkid: &GUID) -> WinResult<()> { + println!("NetworkDeleted {networkid:?}"); + Ok(()) + } + + fn NetworkConnectivityChanged( + &self, + networkid: &GUID, + newconnectivity: NLM_CONNECTIVITY, + ) -> WinResult<()> { + println!("NetworkConnectivityChanged {networkid:?} {newconnectivity:?}"); + Ok(()) + } + + fn NetworkPropertyChanged( + &self, + networkid: &GUID, + flags: NLM_NETWORK_PROPERTY_CHANGE, + ) -> WinResult<()> { + println!("NetworkPropertyChanged {networkid:?} {flags:?}"); + Ok(()) + } +}