From 2487d668af921d43bcce14643cf5aaf07a822b37 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 11:58:52 +0100 Subject: [PATCH 1/9] fix(netwatch): advance buffer on version mismatch in BSD parse_rib --- netwatch/src/interfaces/bsd.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/netwatch/src/interfaces/bsd.rs b/netwatch/src/interfaces/bsd.rs index 3db2b6f3..7f6c0867 100644 --- a/netwatch/src/interfaces/bsd.rs +++ b/netwatch/src/interfaces/bsd.rs @@ -461,7 +461,8 @@ pub fn parse_rib(typ: RIBType, data: &[u8]) -> Result, RouteErr ensure!(l != 0, RouteError::InvalidMessage); ensure!(b.len() >= l as usize, RouteError::MessageTooShort); if b[2] as i32 != ROUTING_STACK.rtm_version { - // b = b[l:]; + b = &b[l as usize..]; + nskips += 1; continue; } match ROUTING_STACK.wire_formats.get(&(b[3] as i32)) { @@ -1015,6 +1016,26 @@ fn parse_default_addr(b: &[u8]) -> Result { mod tests { use super::*; + #[test] + fn test_parse_rib_skips_version_mismatch() { + let wrong_version = (ROUTING_STACK.rtm_version as u8).wrapping_add(1); + let msg_len: u16 = 8; + let mut buf = vec![0u8; msg_len as usize]; + buf[..2].copy_from_slice(&msg_len.to_ne_bytes()); + buf[2] = wrong_version; + buf[3] = 0; // arbitrary type + + #[cfg(any(target_os = "macos", target_os = "ios"))] + let rib_type = libc::NET_RT_IFLIST2; + #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] + let rib_type = libc::NET_RT_IFLIST; + #[cfg(target_os = "openbsd")] + let rib_type = libc::NET_RT_IFLIST; + + let msgs = parse_rib(rib_type, &buf).unwrap(); + assert!(msgs.is_empty(), "version-mismatched message should be skipped"); + } + #[test] fn test_fetch_parse_routing_table() { let rib_raw = fetch_routing_table().unwrap(); From 8785aa1dd2f7fc1b5e4affe78a66ec6f4e25b2e4 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 11:59:24 +0100 Subject: [PATCH 2/9] fix(netwatch): detect prefix count changes in prefixes_major_equal --- netwatch/src/interfaces.rs | 51 +++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/netwatch/src/interfaces.rs b/netwatch/src/interfaces.rs index 8d6448df..32953747 100644 --- a/netwatch/src/interfaces.rs +++ b/netwatch/src/interfaces.rs @@ -417,16 +417,10 @@ fn prefixes_major_equal(a: impl Iterator, b: impl Iterator = a.filter(is_interesting).collect(); + let b: Vec<_> = b.filter(is_interesting).collect(); - for (a, b) in a.zip(b) { - if a != b { - return false; - } - } - - true + a == b } #[cfg(test)] @@ -449,6 +443,45 @@ mod tests { println!("home router: {home_router:#?}"); } + #[test] + fn test_prefixes_major_equal() { + use std::net::Ipv4Addr; + + let a1 = IpNet::V4(Ipv4Net::new(Ipv4Addr::new(192, 168, 0, 1), 24).unwrap()); + let a2 = IpNet::V4(Ipv4Net::new(Ipv4Addr::new(10, 0, 0, 1), 8).unwrap()); + let a3 = IpNet::V4(Ipv4Net::new(Ipv4Addr::new(172, 16, 0, 1), 16).unwrap()); + + // equal lists + assert!(prefixes_major_equal( + vec![a1.clone(), a2.clone()].into_iter(), + vec![a1.clone(), a2.clone()].into_iter(), + )); + + // both empty + assert!(prefixes_major_equal( + std::iter::empty(), + std::iter::empty(), + )); + + // different prefixes + assert!(!prefixes_major_equal( + vec![a1.clone()].into_iter(), + vec![a2.clone()].into_iter(), + )); + + // a has extra prefix + assert!(!prefixes_major_equal( + vec![a1.clone(), a2.clone(), a3.clone()].into_iter(), + vec![a1.clone(), a2.clone()].into_iter(), + )); + + // b has extra prefix + assert!(!prefixes_major_equal( + vec![a1.clone(), a2.clone()].into_iter(), + vec![a1.clone(), a2.clone(), a3.clone()].into_iter(), + )); + } + #[test] fn test_is_usable_v6() { let loopback = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0x1); From da5151506b0e2e9a2742a12894f8f9e31afb2914 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 12:00:03 +0100 Subject: [PATCH 3/9] fix(netwatch): detect new interfaces in is_major_change --- netwatch/src/interfaces.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/netwatch/src/interfaces.rs b/netwatch/src/interfaces.rs index 32953747..f5b52aeb 100644 --- a/netwatch/src/interfaces.rs +++ b/netwatch/src/interfaces.rs @@ -292,6 +292,16 @@ impl State { } } + // Check for new interesting interfaces not present in old state + for (iname, i) in &self.interfaces { + if !is_interesting_interface(i.name()) { + continue; + } + if !old.interfaces.contains_key(iname) { + return true; + } + } + false } } @@ -429,6 +439,33 @@ mod tests { use super::*; + #[test] + fn test_is_major_change_identical() { + let a = State::fake(); + let b = State::fake(); + assert!(!a.is_major_change(&b)); + } + + #[test] + fn test_is_major_change_new_interface_added() { + let old = State::fake(); + let mut new = State::fake(); + // Add a new interesting interface to new state + let mut iface = Interface::fake(); + iface.iface.index = 10; + iface.iface.name = "eth1".to_string(); + new.interfaces.insert("eth1".to_string(), iface); + assert!(new.is_major_change(&old)); + } + + #[test] + fn test_is_major_change_interface_removed() { + let old = State::fake(); + let mut new = State::fake(); + new.interfaces.clear(); + assert!(new.is_major_change(&old)); + } + #[tokio::test] async fn test_default_route() { let default_route = DefaultRouteDetails::new() From 10eb2c82c70b5b5165e9a46c1740e0acb2fbfa4c Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 12:00:31 +0100 Subject: [PATCH 4/9] fix(netwatch): prevent time_jumped signal loss in actor debounce --- netwatch/src/netmon/actor.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/netwatch/src/netmon/actor.rs b/netwatch/src/netmon/actor.rs index c2e55530..10a0843c 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -90,7 +90,8 @@ impl Actor { pub(super) async fn run(mut self) { const DEBOUNCE: Duration = Duration::from_millis(250); - let mut last_event = None; + let mut pending_change = false; + let mut pending_time_jump = false; let mut debounce_interval = time::interval(DEBOUNCE); let mut wall_time_interval = time::interval(POLL_WALL_TIME_INTERVAL); @@ -99,15 +100,16 @@ impl Actor { biased; _ = debounce_interval.tick() => { - if let Some(time_jumped) = last_event.take() { - self.handle_potential_change(time_jumped).await; + if pending_change || pending_time_jump { + self.handle_potential_change(pending_time_jump).await; + pending_change = false; + pending_time_jump = false; } } _ = wall_time_interval.tick() => { trace!("tick: wall_time_interval"); if self.check_wall_time_advance() { - // Trigger potential change - last_event.replace(true); + pending_time_jump = true; debounce_interval.reset_immediately(); } } @@ -115,7 +117,7 @@ impl Actor { match event { Some(NetworkMessage::Change) => { trace!("network activity detected"); - last_event.replace(false); + pending_change = true; debounce_interval.reset_immediately(); } None => { @@ -128,7 +130,7 @@ impl Actor { match msg { Some(ActorMessage::NetworkChange) => { trace!("external network activity detected"); - last_event.replace(false); + pending_change = true; debounce_interval.reset_immediately(); } None => { From f3986e3a001340438d746c215b3f97db8abed06b Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 12:01:06 +0100 Subject: [PATCH 5/9] fix(netwatch): replace interval reset with sleep-based debounce for true coalescing --- netwatch/src/netmon/actor.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/netwatch/src/netmon/actor.rs b/netwatch/src/netmon/actor.rs index 10a0843c..e4dc0000 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -92,25 +92,22 @@ impl Actor { let mut pending_change = false; let mut pending_time_jump = false; - let mut debounce_interval = time::interval(DEBOUNCE); + let debounce = time::sleep(DEBOUNCE); + tokio::pin!(debounce); let mut wall_time_interval = time::interval(POLL_WALL_TIME_INTERVAL); loop { tokio::select! { - biased; - - _ = debounce_interval.tick() => { - if pending_change || pending_time_jump { - self.handle_potential_change(pending_time_jump).await; - pending_change = false; - pending_time_jump = false; - } + _ = &mut debounce, if pending_change || pending_time_jump => { + self.handle_potential_change(pending_time_jump).await; + pending_change = false; + pending_time_jump = false; } _ = wall_time_interval.tick() => { trace!("tick: wall_time_interval"); if self.check_wall_time_advance() { pending_time_jump = true; - debounce_interval.reset_immediately(); + debounce.as_mut().reset(Instant::now() + DEBOUNCE); } } event = self.mon_receiver.recv() => { @@ -118,7 +115,7 @@ impl Actor { Some(NetworkMessage::Change) => { trace!("network activity detected"); pending_change = true; - debounce_interval.reset_immediately(); + debounce.as_mut().reset(Instant::now() + DEBOUNCE); } None => { debug!("shutting down, network monitor receiver gone"); @@ -131,7 +128,7 @@ impl Actor { Some(ActorMessage::NetworkChange) => { trace!("external network activity detected"); pending_change = true; - debounce_interval.reset_immediately(); + debounce.as_mut().reset(Instant::now() + DEBOUNCE); } None => { debug!("shutting down, actor receiver gone"); From 68a334f5b00d390d32e3375aa2b1fe25be952de9 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 12:01:41 +0100 Subject: [PATCH 6/9] fix(netwatch): cancel Windows callbacks before dropping Arc --- netwatch/src/netmon/windows.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/netwatch/src/netmon/windows.rs b/netwatch/src/netmon/windows.rs index d97b6b50..87fd575f 100644 --- a/netwatch/src/netmon/windows.rs +++ b/netwatch/src/netmon/windows.rs @@ -129,15 +129,15 @@ impl CallbackHandler { handle: UnicastCallbackHandle, ) -> Result<(), Error> { trace!("unregistering unicast callback"); - if self - .unicast_callbacks - .remove(&(handle.0.0 as isize)) - .is_some() - { + let key = handle.0.0 as isize; + if self.unicast_callbacks.contains_key(&key) { + // Cancel first to ensure no in-flight callbacks reference the Arc, + // then remove the Arc from the map. unsafe { windows::Win32::NetworkManagement::IpHelper::CancelMibChangeNotify2(handle.0) .ok()?; } + self.unicast_callbacks.remove(&key); } Ok(()) @@ -171,15 +171,15 @@ impl CallbackHandler { handle: RouteCallbackHandle, ) -> Result<(), Error> { trace!("unregistering route callback"); - if self - .route_callbacks - .remove(&(handle.0.0 as isize)) - .is_some() - { + let key = handle.0.0 as isize; + if self.route_callbacks.contains_key(&key) { + // Cancel first to ensure no in-flight callbacks reference the Arc, + // then remove the Arc from the map. unsafe { windows::Win32::NetworkManagement::IpHelper::CancelMibChangeNotify2(handle.0) .ok()?; } + self.route_callbacks.remove(&key); } Ok(()) From da22bc8ee498d7c34acc1ff371d4a47c1d31a954 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 12:12:29 +0100 Subject: [PATCH 7/9] style: fix formatting in test assertions --- netwatch/src/interfaces.rs | 5 +---- netwatch/src/interfaces/bsd.rs | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/netwatch/src/interfaces.rs b/netwatch/src/interfaces.rs index f5b52aeb..119d73fe 100644 --- a/netwatch/src/interfaces.rs +++ b/netwatch/src/interfaces.rs @@ -495,10 +495,7 @@ mod tests { )); // both empty - assert!(prefixes_major_equal( - std::iter::empty(), - std::iter::empty(), - )); + assert!(prefixes_major_equal(std::iter::empty(), std::iter::empty(),)); // different prefixes assert!(!prefixes_major_equal( diff --git a/netwatch/src/interfaces/bsd.rs b/netwatch/src/interfaces/bsd.rs index 7f6c0867..bed3cbed 100644 --- a/netwatch/src/interfaces/bsd.rs +++ b/netwatch/src/interfaces/bsd.rs @@ -1033,7 +1033,10 @@ mod tests { let rib_type = libc::NET_RT_IFLIST; let msgs = parse_rib(rib_type, &buf).unwrap(); - assert!(msgs.is_empty(), "version-mismatched message should be skipped"); + assert!( + msgs.is_empty(), + "version-mismatched message should be skipped" + ); } #[test] From 6504ae0ffe80002409962ff08e02abb0114999fc Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 13:13:34 +0100 Subject: [PATCH 8/9] fix(netwatch): non-allocating prefixes_major_equal --- netwatch/src/interfaces.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/netwatch/src/interfaces.rs b/netwatch/src/interfaces.rs index 119d73fe..44a210c1 100644 --- a/netwatch/src/interfaces.rs +++ b/netwatch/src/interfaces.rs @@ -427,10 +427,16 @@ fn prefixes_major_equal(a: impl Iterator, b: impl Iterator = a.filter(is_interesting).collect(); - let b: Vec<_> = b.filter(is_interesting).collect(); - - a == b + let mut a = a.filter(is_interesting); + let mut b = b.filter(is_interesting); + + loop { + match (a.next(), b.next()) { + (None, None) => return true, + (Some(a), Some(b)) if a == b => continue, + _ => return false, + } + } } #[cfg(test)] From 0e1890d2e8add4e95e5b0f359b400d7d8ab0729b Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 26 Feb 2026 13:18:44 +0100 Subject: [PATCH 9/9] deps: bump wasm-bindgen-test to 0.3.62 --- Cargo.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d5854a2..973807c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -788,7 +788,7 @@ dependencies = [ "libc", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -799,9 +799,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "f4eacb0641a310445a4c513f2a5e23e19952e269c6a38887254d5f837a305506" dependencies = [ "once_cell", "wasm-bindgen", @@ -1113,7 +1113,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1952,9 +1952,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "05d7d0fce354c88b7982aec4400b3e7fcf723c32737cef571bd165f7613557ee" dependencies = [ "cfg-if", "once_cell", @@ -1965,9 +1965,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "ee85afca410ac4abba5b584b12e77ea225db6ee5471d0aebaae0861166f9378a" dependencies = [ "cfg-if", "futures-util", @@ -1979,9 +1979,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "55839b71ba921e4f75b674cb16f843f4b1f3b26ddfcb3454de1cf65cc021ec0f" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1989,9 +1989,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "caf2e969c2d60ff52e7e98b7392ff1588bffdd1ccd4769eba27222fd3d621571" dependencies = [ "bumpalo", "proc-macro2", @@ -2002,18 +2002,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "0861f0dcdf46ea819407495634953cdcc8a8c7215ab799a7a7ce366be71c7b30" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.58" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45649196a53b0b7a15101d845d44d2dda7374fc1b5b5e2bbf58b7577ff4b346d" +checksum = "12430eab93df2be01b6575bf8e05700945dafa62d6fa40faa07b0ea9afd8add1" dependencies = [ "async-trait", "cast", @@ -2033,9 +2033,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.58" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579cdd0123ac74b94e1a4a72bd963cf30ebac343f2df347da0b8df24cdebed2" +checksum = "ce7d6debc1772c3502c727c8c47180c040c8741f7fcf6e731d6ef57818d59ae2" dependencies = [ "proc-macro2", "quote", @@ -2044,9 +2044,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea" +checksum = "c4f79c547a8daa04318dac7646f579a016f819452c34bcb14e8dda0e77a4386c" [[package]] name = "wasm-tracing" @@ -2061,9 +2061,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "10053fbf9a374174094915bbce141e87a6bf32ecd9a002980db4b638405e8962" dependencies = [ "js-sys", "wasm-bindgen", @@ -2101,7 +2101,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]]