feat(windows): new module to listen for network changes (#3137)

This isn't hooked up to the GUI yet, it's a debug subcommand.

I overheard that the other clients rebuild the tunnel when they change
networks, I think? And this might be useful for debugging the issue
where Chrome / other browsers don't flush their TCP connections when the
tunnel comes up. It's also reference code for how to use COM interfaces
in Rust. The official samples are a little sparse. So I wanted to get
this checked in.


![image](https://github.com/firezone/firezone/assets/13400041/9f9c576e-c56f-4d7c-93f4-6e92eace5914)
This commit is contained in:
Reactor Scram
2024-01-09 14:58:54 -06:00
committed by GitHub
parent 2a2cfd93f0
commit 33133d7448
6 changed files with 154 additions and 6 deletions

33
rust/Cargo.lock generated
View File

@@ -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]]

View File

@@ -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",
]

View File

@@ -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

View File

@@ -14,6 +14,7 @@ pub enum CliCommands {
Debug,
DebugCrash,
DebugHostname,
DebugNetworkChanges,
DebugPipeServer,
DebugWintun,
Elevated,

View File

@@ -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();

View File

@@ -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<u32>,
/// 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<Self> {
// `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(())
}
}