feat(windows): optimise network change detection (#9021)

Presently, the network change detection on Windows is very naive and
simply emits a change event everytime _anything_ changes. We can
optimise this and therefore improve the start-up time of Firezone by:

- Filtering out duplicate events
- Filtering out network change events for our own network adapter

This reduces the number of network change events to 1 during startup. As
far as I can tell from the code comments in this area, we explicitly
send this one to ensure we don't run into a race condition whilst we are
starting up.

Resolves: #8905
This commit is contained in:
Thomas Eizinger
2025-05-06 10:23:27 +10:00
committed by GitHub
parent 806996c245
commit 005b6fe863
3 changed files with 81 additions and 5 deletions

View File

@@ -64,6 +64,8 @@
use crate::platform::DnsControlMethod;
use anyhow::{Context as _, Result, anyhow};
use std::collections::HashMap;
use std::sync::Mutex;
use std::thread;
use tokio::sync::{
mpsc::{self, error::TrySendError},
@@ -251,6 +253,8 @@ struct Listener<'a> {
#[windows_implement::implement(INetworkEvents)]
struct Callback {
tx: NotifySender,
firezone_network_profile_id: Option<GUID>,
network_states: Mutex<HashMap<GUID, NLM_CONNECTIVITY>>,
}
impl Drop for Listener<'_> {
@@ -294,7 +298,13 @@ impl<'a> Listener<'a> {
_com: com,
};
let cb = Callback { tx: tx.clone() };
let cb = Callback {
tx: tx.clone(),
firezone_network_profile_id: get_network_id_of_firezone_adapter()
.inspect_err(|e| tracing::warn!("{e:#}"))
.ok(),
network_states: Default::default(),
};
let callbacks: INetworkEvents = cb.into();
@@ -340,6 +350,35 @@ impl<'a> Listener<'a> {
}
}
fn get_network_id_of_firezone_adapter() -> Result<GUID> {
let profiles = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE)
.open_subkey(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles")
.context("Failed to open registry key")?;
for key in profiles.enum_keys() {
let guid = key.context("Failed to enumerate key")?;
let profile_name = profiles
.open_subkey(&guid)
.with_context(|| format!("Failed to open key `{guid}`"))?
.get_value::<String, _>("ProfileName")
.context("Failed to get profile name")?;
if profile_name == "Firezone" {
let uuid = guid.trim_start_matches("{").trim_end_matches("}");
let uuid = uuid
.parse::<uuid::Uuid>()
.context("Failed to parse key as UUID")?;
tracing::debug!(%uuid, "Found `Firezone` network profile id");
return Ok(GUID::from_u128(uuid.as_u128()));
}
}
anyhow::bail!("Unable to find `Firezone` profile network GUID")
}
// <https://github.com/microsoft/windows-rs/pull/3065>
impl INetworkEvents_Impl for Callback_Impl {
fn NetworkAdded(&self, _networkid: &GUID) -> WinResult<()> {
@@ -352,9 +391,34 @@ impl INetworkEvents_Impl for Callback_Impl {
fn NetworkConnectivityChanged(
&self,
_networkid: &GUID,
_newconnectivity: NLM_CONNECTIVITY,
networkid: &GUID,
newconnectivity: NLM_CONNECTIVITY,
) -> WinResult<()> {
if self
.firezone_network_profile_id
.is_some_and(|firezone| networkid == &firezone)
{
tracing::debug!("Ignoring network change for `Firezone` adapter");
return Ok(());
}
let mut network_states = self
.network_states
.lock()
.unwrap_or_else(|e| e.into_inner());
if network_states
.get(networkid)
.is_some_and(|state| *state == newconnectivity)
{
tracing::debug!(?networkid, "Ignoring duplicate network change");
return Ok(());
}
network_states.insert(*networkid, newconnectivity);
tracing::debug!(?networkid, ?newconnectivity, "Network connectivity changed");
// No reasonable way to translate this error into a Windows error
self.tx.notify().ok();
Ok(())

View File

@@ -8,7 +8,13 @@ export default function GUI({ os }: { os: OS }) {
return (
<Entries downloadLinks={downloadLinks(os)} title={title(os)}>
{/* When you cut a release, remove any solved issues from the "known issues" lists over in `client-apps`. This must not be done when the issue's PR merges. */}
<Unreleased></Unreleased>
<Unreleased>
{os === OS.Windows && (
<ChangeItem pull="9021">
Optimizes network change detection.
</ChangeItem>
)}
</Unreleased>
<Entry version="1.4.12" date={new Date("2025-04-30")}>
{os === OS.Linux && (
<ChangeItem pull="8731">

View File

@@ -9,7 +9,13 @@ export default function Headless({ os }: { os: OS }) {
return (
<Entries downloadLinks={downloadLinks(os)} title={title(os)}>
{/* When you cut a release, remove any solved issues from the "known issues" lists over in `client-apps`. This must not be done when the issue's PR merges. */}
<Unreleased></Unreleased>
<Unreleased>
{os === OS.Windows && (
<ChangeItem pull="9021">
Optimizes network change detection.
</ChangeItem>
)}
</Unreleased>
<Entry version="1.4.7" date={new Date("2025-04-30")}>
<ChangeItem pull="8798">
Improves performance of relayed connections on IPv4-only systems.