pax_global_header00006660000000000000000000000064145272145620014522gustar00rootroot0000000000000052 comment=f21df8166f79d215443823c381ea8b8336d85e3c if-watch-3.2.0/000077500000000000000000000000001452721456200132265ustar00rootroot00000000000000if-watch-3.2.0/.github/000077500000000000000000000000001452721456200145665ustar00rootroot00000000000000if-watch-3.2.0/.github/workflows/000077500000000000000000000000001452721456200166235ustar00rootroot00000000000000if-watch-3.2.0/.github/workflows/ci.yml000066400000000000000000000055061452721456200177470ustar00rootroot00000000000000on: [pull_request] name: if-watch jobs: ci: strategy: fail-fast: false matrix: toolchain: - rust: stable #- rust: nightly platform: - target: x86_64-unknown-linux-gnu host: ubuntu-latest cross: false - target: x86_64-apple-darwin host: macos-latest cross: false - target: x86_64-pc-windows-msvc host: windows-latest cross: false - target: armv7-linux-androideabi host: ubuntu-latest cross: true - target: aarch64-linux-android host: ubuntu-latest cross: true - target: aarch64-apple-ios host: macos-latest cross: true env: RUST_BACKTRACE: 1 CARGO_INCREMENTAL: 0 LLVM_CONFIG_PATH: /usr/local/opt/llvm/bin/llvm-config NDK_HOME: /usr/local/lib/android/sdk/ndk-bundle runs-on: ${{ matrix.platform.host }} steps: - name: Checkout sources uses: actions/checkout@v2 - name: Cache cargo folder uses: actions/cache@v1 with: path: ~/.cargo key: ${{ matrix.platform.target }}-cargo-${{ matrix.toolchain.rust }} - name: Install dependencies ubuntu if: matrix.platform.host == 'ubuntu-latest' run: sudo apt-get install llvm-dev - name: Install dependencies macos if: matrix.platform.host == 'macos-latest' run: brew install llvm - name: Install dependencies windows if: matrix.platform.host == 'windows-latest' run: choco install llvm - name: Install rust toolchain uses: hecrj/setup-rust-action@v1 with: rust-version: ${{ matrix.toolchain.rust }} targets: ${{ matrix.platform.target }} - name: Install cargo-apk if: contains(matrix.platform.target, 'android') run: cargo install cargo-apk - name: Build if: contains(matrix.platform.target, 'android') == false run: cargo build --workspace --all-features --target ${{ matrix.platform.target }} - name: Build android if: contains(matrix.platform.target, 'android') run: cargo apk build --target ${{ matrix.platform.target }} --all-features - name: Rust tests if: matrix.platform.cross == false run: cargo test --workspace --all-features lint-rust: runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Cache cargo folder uses: actions/cache@v1 with: path: ~/.cargo key: lint-cargo - name: Install rust toolchain uses: hecrj/setup-rust-action@v1 with: rust-version: stable components: clippy, rustfmt - name: cargo fmt run: cargo fmt --all -- --check - name: cargo clippy run: cargo clippy --workspace --all-features --examples --tests -- -D warnings if-watch-3.2.0/.gitignore000066400000000000000000000000231452721456200152110ustar00rootroot00000000000000/target Cargo.lock if-watch-3.2.0/CHANGELOG.md000066400000000000000000000037111452721456200150410ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [3.2.0] ### Fixed - Update `async-io`, `if-addrs` crates. See [PR 33](https://github.com/mxinden/if-watch/pull/33). ## [3.1.0] ### Fixed - Update `windows` crate. See [PR 32](https://github.com/mxinden/if-watch/pull/32). ## [3.0.1] ### Fixed - For all architectures running the fallback option (e.g. Android) reverse the logic when checking if a recorded interface still exists in the new list to avoid reporting all interfaces as down and then up in the same resync(). See [PR 31]. [PR 31]: https://github.com/mxinden/if-watch/pull/31 ## [3.0.0] ### Changed - Feature gate async runtime, allowing opting between Tokio or smol. For every OS each `IfWatcher` is under the `tokio` or `smol` module. This makes it a breaking change as there is no more a default implementation. See [PR 27](https://github.com/mxinden/if-watch/pull/27). ## [2.0.0] ### Changed - Add `IfWatcher::poll_if_event`. Implement `Stream` instead of `Future` for `IfWatcher`. See [PR 23] and [PR 25]. - Make `IfWatcher::new` synchronous. See [PR 24]. [PR 23]: https://github.com/mxinden/if-watch/pull/23 [PR 24]: https://github.com/mxinden/if-watch/pull/24 [PR 25]: https://github.com/mxinden/if-watch/pull/25 ## [1.1.1] ### Fixed - Update to `rtnetlink` `v0.10`. See [PR 19]. [PR 19]: https://github.com/mxinden/if-watch/pull/19 ## [1.1.0] ### Added - Return socket closure as error. See [PR 15]. ### Fixed - Update to `windows` `v0.34`. See [PR 16]. [PR 15]: https://github.com/mxinden/if-watch/pull/15 [PR 16]: https://github.com/mxinden/if-watch/pull/16 ## [1.0.0] - 2022-01-12 ### Added - macos/ios backend ### Changed - linux backend rewritten to use rtnetlink - windows backend rewritten to use windows crate instead of winapi if-watch-3.2.0/Cargo.toml000066400000000000000000000027431452721456200151640ustar00rootroot00000000000000[package] name = "if-watch" version = "3.2.0" authors = ["David Craven ", "Parity Technologies Limited "] edition = "2021" keywords = ["asynchronous", "routing"] license = "MIT OR Apache-2.0" description = "crossplatform asynchronous network watcher" repository = "https://github.com/mxinden/if-watch" [lib] crate-type = ["cdylib", "lib"] [features] tokio = ["dep:tokio", "rtnetlink/tokio_socket"] smol = ["dep:smol", "rtnetlink/smol_socket"] [dependencies] fnv = "1.0.7" futures = "0.3.19" ipnet = "2.3.1" log = "0.4.14" [target.'cfg(target_os = "linux")'.dependencies] rtnetlink = { version = "0.10.0", default-features = false } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] core-foundation = "0.9.2" if-addrs = "0.10.0" system-configuration = "0.5.0" tokio = { version = "1.21.2", features = ["rt"], optional = true } smol = { version = "1.2.5", optional = true } [target.'cfg(target_os = "windows")'.dependencies] if-addrs = "0.10.0" windows = { version = "0.51.0", features = ["Win32_NetworkManagement_IpHelper", "Win32_Foundation", "Win32_NetworkManagement_Ndis", "Win32_Networking_WinSock"] } [target.'cfg(not(any(target_os = "ios", target_os = "linux", target_os = "macos", target_os = "windows")))'.dependencies] async-io = "2.0.0" if-addrs = "0.10.0" [dev-dependencies] env_logger = "0.10.0" smol = "1.2.5" tokio = { version = "1.21.2", features = ["rt", "macros"] } [[example]] name = "if_watch" required-features = ["smol"] if-watch-3.2.0/README.md000066400000000000000000000007011452721456200145030ustar00rootroot00000000000000# Cross platform asynchronous network watcher ```sh cargo run --example if_watch Got event Ok(Up(127.0.0.1/8)) Got event Ok(Up(192.168.6.65/24)) Got event Ok(Up(::1/128)) Got event Ok(Up(2a01:8b81:7000:9700:cef9:e4ff:fe9e:b23b/64)) Got event Ok(Up(fe80::cef9:e4ff:fe9e:b23b/64)) ``` Supported platforms at the moment are: Linux, Windows and Android with a fallback for Macos and ios that polls for changes every 10s. ## License MIT OR Apache-2.0 if-watch-3.2.0/examples/000077500000000000000000000000001452721456200150445ustar00rootroot00000000000000if-watch-3.2.0/examples/if_watch.rs000066400000000000000000000004601452721456200171760ustar00rootroot00000000000000use futures::StreamExt; use if_watch::smol::IfWatcher; fn main() { env_logger::init(); smol::block_on(async { let mut set = IfWatcher::new().unwrap(); loop { let event = set.select_next_some().await; println!("Got event {:?}", event); } }); } if-watch-3.2.0/src/000077500000000000000000000000001452721456200140155ustar00rootroot00000000000000if-watch-3.2.0/src/apple.rs000066400000000000000000000111251452721456200154640ustar00rootroot00000000000000use crate::{IfEvent, IpNet, Ipv4Net, Ipv6Net}; use core_foundation::array::CFArray; use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop}; use core_foundation::string::CFString; use fnv::FnvHashSet; use futures::channel::mpsc; use futures::stream::{FusedStream, Stream}; use if_addrs::IfAddr; use std::collections::VecDeque; use std::io::Result; use std::pin::Pin; use std::task::{Context, Poll}; use system_configuration::dynamic_store::{ SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext, }; #[cfg(feature = "tokio")] pub mod tokio { //! An interface watcher. //! **On Apple Platforms there is no difference between `tokio` and `smol` features,** //! **this was done to maintain the api compatible with other platforms**. /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } #[cfg(feature = "smol")] pub mod smol { //! An interface watcher. //! **On Apple platforms there is no difference between `tokio` and `smol` features,** //! **this was done to maintain the api compatible with other platforms**. /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } #[derive(Debug)] pub struct IfWatcher { addrs: FnvHashSet, queue: VecDeque, rx: mpsc::Receiver<()>, } impl IfWatcher { pub fn new() -> Result { let (tx, rx) = mpsc::channel(1); std::thread::spawn(|| background_task(tx)); let mut watcher = Self { addrs: Default::default(), queue: Default::default(), rx, }; watcher.resync()?; Ok(watcher) } fn resync(&mut self) -> Result<()> { let addrs = if_addrs::get_if_addrs()?; for old_addr in self.addrs.clone() { if addrs .iter() .find(|addr| addr.ip() == old_addr.addr()) .is_none() { self.addrs.remove(&old_addr); self.queue.push_back(IfEvent::Down(old_addr)); } } for new_addr in addrs { let ipnet = ifaddr_to_ipnet(new_addr.addr); if self.addrs.insert(ipnet) { self.queue.push_back(IfEvent::Up(ipnet)); } } Ok(()) } /// Iterate over current networks. pub fn iter(&self) -> impl Iterator { self.addrs.iter() } /// Poll for an address change event. pub fn poll_if_event(&mut self, cx: &mut Context) -> Poll> { loop { if let Some(event) = self.queue.pop_front() { return Poll::Ready(Ok(event)); } if Pin::new(&mut self.rx).poll_next(cx).is_pending() { return Poll::Pending; } if let Err(error) = self.resync() { return Poll::Ready(Err(error)); } } } } impl Stream for IfWatcher { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::into_inner(self).poll_if_event(cx).map(Some) } } impl FusedStream for IfWatcher { fn is_terminated(&self) -> bool { false } } fn ifaddr_to_ipnet(addr: IfAddr) -> IpNet { match addr { IfAddr::V4(ip) => { let prefix_len = (!u32::from_be_bytes(ip.netmask.octets())).leading_zeros(); IpNet::V4( Ipv4Net::new(ip.ip, prefix_len as u8).expect("if_addrs returned a valid prefix"), ) } IfAddr::V6(ip) => { let prefix_len = (!u128::from_be_bytes(ip.netmask.octets())).leading_zeros(); IpNet::V6( Ipv6Net::new(ip.ip, prefix_len as u8).expect("if_addrs returned a valid prefix"), ) } } } fn callback(_store: SCDynamicStore, _changed_keys: CFArray, info: &mut mpsc::Sender<()>) { match info.try_send(()) { Err(err) if err.is_disconnected() => CFRunLoop::get_current().stop(), _ => {} } } fn background_task(tx: mpsc::Sender<()>) { let store = SCDynamicStoreBuilder::new("global-network-watcher") .callback_context(SCDynamicStoreCallBackContext { callout: callback, info: tx, }) .build(); store.set_notification_keys( &CFArray::::from_CFTypes(&[]), &CFArray::from_CFTypes(&[CFString::new("State:/Network/Interface/.*/IPv.")]), ); let source = store.create_run_loop_source(); let run_loop = CFRunLoop::get_current(); run_loop.add_source(&source, unsafe { kCFRunLoopCommonModes }); CFRunLoop::run_current(); } if-watch-3.2.0/src/fallback.rs000066400000000000000000000066271452721456200161350ustar00rootroot00000000000000use crate::IfEvent; use async_io::Timer; use futures::stream::{FusedStream, Stream}; use if_addrs::IfAddr; use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use std::collections::{HashSet, VecDeque}; use std::io::Result; use std::pin::Pin; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; #[cfg(feature = "tokio")] pub mod tokio { //! An interface watcher. //! **On this platform there is no difference between `tokio` and `smol` features,** //! **this was done to maintain the api compatible with other platforms**. /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } #[cfg(feature = "smol")] pub mod smol { //! An interface watcher. //! **On this platform there is no difference between `tokio` and `smol` features,** //! **this was done to maintain the api compatible with other platforms**. /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } /// An address set/watcher #[derive(Debug)] pub struct IfWatcher { addrs: HashSet, queue: VecDeque, ticker: Timer, } impl IfWatcher { /// Create a watcher. pub fn new() -> Result { Ok(Self { addrs: Default::default(), queue: Default::default(), ticker: Timer::interval_at(Instant::now(), Duration::from_secs(10)), }) } fn resync(&mut self) -> Result<()> { let addrs = if_addrs::get_if_addrs()?; for old_addr in self.addrs.clone() { if !addrs.iter().any(|addr| addr.ip() == old_addr.addr()) { self.addrs.remove(&old_addr); self.queue.push_back(IfEvent::Down(old_addr)); } } for new_addr in addrs { let ipnet = ifaddr_to_ipnet(new_addr.addr); if self.addrs.insert(ipnet) { self.queue.push_back(IfEvent::Up(ipnet)); } } Ok(()) } /// Iterate over current networks. pub fn iter(&self) -> impl Iterator { self.addrs.iter() } /// Poll for an address change event. pub fn poll_if_event(&mut self, cx: &mut Context) -> Poll> { loop { if let Some(event) = self.queue.pop_front() { return Poll::Ready(Ok(event)); } if Pin::new(&mut self.ticker).poll_next(cx).is_pending() { return Poll::Pending; } if let Err(err) = self.resync() { return Poll::Ready(Err(err)); } } } } impl Stream for IfWatcher { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::into_inner(self).poll_if_event(cx).map(Some) } } impl FusedStream for IfWatcher { fn is_terminated(&self) -> bool { false } } fn ifaddr_to_ipnet(addr: IfAddr) -> IpNet { match addr { IfAddr::V4(ip) => { let prefix_len = (!u32::from_be_bytes(ip.netmask.octets())).leading_zeros(); IpNet::V4( Ipv4Net::new(ip.ip, prefix_len as u8).expect("if_addrs returned a valid prefix"), ) } IfAddr::V6(ip) => { let prefix_len = (!u128::from_be_bytes(ip.netmask.octets())).leading_zeros(); IpNet::V6( Ipv6Net::new(ip.ip, prefix_len as u8).expect("if_addrs returned a valid prefix"), ) } } } if-watch-3.2.0/src/lib.rs000066400000000000000000000052131452721456200151320ustar00rootroot00000000000000//! IP address watching. #![deny(missing_docs)] #![deny(warnings)] pub use ipnet::{IpNet, Ipv4Net, Ipv6Net}; #[cfg(target_os = "macos")] mod apple; #[cfg(target_os = "ios")] mod apple; #[cfg(not(any( target_os = "ios", target_os = "linux", target_os = "macos", target_os = "windows", )))] mod fallback; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "windows")] mod win; #[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(feature = "tokio")] pub use apple::tokio; #[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(feature = "smol")] pub use apple::smol; #[cfg(feature = "smol")] #[cfg(not(any( target_os = "ios", target_os = "linux", target_os = "macos", target_os = "windows", )))] pub use fallback::smol; #[cfg(feature = "tokio")] #[cfg(not(any( target_os = "ios", target_os = "linux", target_os = "macos", target_os = "windows", )))] pub use fallback::tokio; #[cfg(target_os = "windows")] #[cfg(feature = "tokio")] pub use win::tokio; #[cfg(target_os = "windows")] #[cfg(feature = "smol")] pub use win::smol; #[cfg(target_os = "linux")] #[cfg(feature = "tokio")] pub use linux::tokio; #[cfg(target_os = "linux")] #[cfg(feature = "smol")] pub use linux::smol; /// An address change event. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum IfEvent { /// A new local address has been added. Up(IpNet), /// A local address has been deleted. Down(IpNet), } #[cfg(test)] mod tests { use futures::StreamExt; use std::pin::Pin; #[test] fn test_smol_ip_watch() { use super::smol::IfWatcher; smol::block_on(async { let mut set = IfWatcher::new().unwrap(); let event = set.select_next_some().await.unwrap(); println!("Got event {:?}", event); }); } #[tokio::test] async fn test_tokio_ip_watch() { use super::tokio::IfWatcher; let mut set = IfWatcher::new().unwrap(); let event = set.select_next_some().await.unwrap(); println!("Got event {:?}", event); } #[test] fn test_smol_is_send() { use super::smol::IfWatcher; smol::block_on(async { fn is_send(_: T) {} is_send(IfWatcher::new()); is_send(IfWatcher::new().unwrap()); is_send(Pin::new(&mut IfWatcher::new().unwrap())); }); } #[tokio::test] async fn test_tokio_is_send() { use super::tokio::IfWatcher; fn is_send(_: T) {} is_send(IfWatcher::new()); is_send(IfWatcher::new().unwrap()); is_send(Pin::new(&mut IfWatcher::new().unwrap())); } } if-watch-3.2.0/src/linux.rs000066400000000000000000000124621452721456200155270ustar00rootroot00000000000000use crate::{IfEvent, IpNet, Ipv4Net, Ipv6Net}; use fnv::FnvHashSet; use futures::ready; use futures::stream::{FusedStream, Stream, TryStreamExt}; use futures::StreamExt; use rtnetlink::constants::{RTMGRP_IPV4_IFADDR, RTMGRP_IPV6_IFADDR}; use rtnetlink::packet::address::nlas::Nla; use rtnetlink::packet::{AddressMessage, RtnlMessage}; use rtnetlink::proto::{Connection, NetlinkPayload}; use rtnetlink::sys::{AsyncSocket, SocketAddr}; use std::collections::VecDeque; use std::future::Future; use std::io::{Error, ErrorKind, Result}; use std::net::{Ipv4Addr, Ipv6Addr}; use std::pin::Pin; use std::task::{Context, Poll}; #[cfg(feature = "tokio")] pub mod tokio { //! An interface watcher that uses `rtnetlink`'s [`TokioSocket`](rtnetlink::sys::TokioSocket) use rtnetlink::sys::TokioSocket; /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } #[cfg(feature = "smol")] pub mod smol { //! An interface watcher that uses `rtnetlink`'s [`SmolSocket`](rtnetlink::sys::SmolSocket) use rtnetlink::sys::SmolSocket; /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } pub struct IfWatcher { conn: Connection, messages: Pin> + Send>>, addrs: FnvHashSet, queue: VecDeque, } impl std::fmt::Debug for IfWatcher { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("IfWatcher") .field("addrs", &self.addrs) .finish_non_exhaustive() } } impl IfWatcher where T: AsyncSocket + Unpin, { /// Create a watcher. pub fn new() -> Result { let (mut conn, handle, messages) = rtnetlink::new_connection_with_socket::()?; let groups = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; let addr = SocketAddr::new(0, groups); conn.socket_mut().socket_mut().bind(&addr)?; let get_addrs_stream = handle .address() .get() .execute() .map_ok(RtnlMessage::NewAddress) .map_err(|err| Error::new(ErrorKind::Other, err)); let msg_stream = messages.filter_map(|(msg, _)| async { match msg.payload { NetlinkPayload::Error(err) => Some(Err(err.to_io())), NetlinkPayload::InnerMessage(msg) => Some(Ok(msg)), _ => None, } }); let messages = get_addrs_stream.chain(msg_stream).boxed(); let addrs = FnvHashSet::default(); let queue = VecDeque::default(); Ok(Self { conn, messages, addrs, queue, }) } /// Iterate over current networks. pub fn iter(&self) -> impl Iterator { self.addrs.iter() } fn add_address(&mut self, msg: AddressMessage) { for net in iter_nets(msg) { if self.addrs.insert(net) { self.queue.push_back(IfEvent::Up(net)); } } } fn rem_address(&mut self, msg: AddressMessage) { for net in iter_nets(msg) { if self.addrs.remove(&net) { self.queue.push_back(IfEvent::Down(net)); } } } /// Poll for an address change event. pub fn poll_if_event(&mut self, cx: &mut Context) -> Poll> { loop { if let Some(event) = self.queue.pop_front() { return Poll::Ready(Ok(event)); } if Pin::new(&mut self.conn).poll(cx).is_ready() { return Poll::Ready(Err(socket_err())); } let message = ready!(self.messages.poll_next_unpin(cx)).ok_or_else(socket_err)??; match message { RtnlMessage::NewAddress(msg) => self.add_address(msg), RtnlMessage::DelAddress(msg) => self.rem_address(msg), _ => {} } } } } fn socket_err() -> std::io::Error { std::io::Error::new(ErrorKind::BrokenPipe, "rtnetlink socket closed") } fn iter_nets(msg: AddressMessage) -> impl Iterator { let prefix = msg.header.prefix_len; let family = msg.header.family; msg.nlas.into_iter().filter_map(move |nla| { if let Nla::Address(octets) = nla { match family { 2 => { let mut addr = [0; 4]; addr.copy_from_slice(&octets); Some(IpNet::V4( Ipv4Net::new(Ipv4Addr::from(addr), prefix).unwrap(), )) } 10 => { let mut addr = [0; 16]; addr.copy_from_slice(&octets); Some(IpNet::V6( Ipv6Net::new(Ipv6Addr::from(addr), prefix).unwrap(), )) } _ => None, } } else { None } }) } impl Stream for IfWatcher where T: AsyncSocket + Unpin, { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::into_inner(self).poll_if_event(cx).map(Some) } } impl FusedStream for IfWatcher where T: AsyncSocket + AsyncSocket + Unpin, { fn is_terminated(&self) -> bool { false } } if-watch-3.2.0/src/win.rs000066400000000000000000000134061452721456200151640ustar00rootroot00000000000000use crate::{IfEvent, IpNet, Ipv4Net, Ipv6Net}; use fnv::FnvHashSet; use futures::stream::{FusedStream, Stream}; use futures::task::AtomicWaker; use if_addrs::IfAddr; use std::collections::VecDeque; use std::ffi::c_void; use std::io::{Error, ErrorKind, Result}; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::task::{Context, Poll}; use windows::Win32::Foundation::{BOOLEAN, HANDLE}; use windows::Win32::NetworkManagement::IpHelper::{ CancelMibChangeNotify2, NotifyIpInterfaceChange, MIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE, }; use windows::Win32::Networking::WinSock::AF_UNSPEC; #[cfg(feature = "tokio")] pub mod tokio { //! An interface watcher. //! **On Windows there is no difference between `tokio` and `smol` features,** //! **this was done to maintain the api compatible with other platforms**. /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } #[cfg(feature = "smol")] pub mod smol { //! An interface watcher. //! **On Windows there is no difference between `tokio` and `smol` features,** //! **this was done to maintain the api compatible with other platforms**. /// Watches for interface changes. pub type IfWatcher = super::IfWatcher; } /// An address set/watcher #[derive(Debug)] pub struct IfWatcher { addrs: FnvHashSet, queue: VecDeque, #[allow(unused)] notif: IpChangeNotification, waker: Arc, resync: Arc, } impl IfWatcher { /// Create a watcher. pub fn new() -> Result { let resync = Arc::new(AtomicBool::new(true)); let waker = Arc::new(AtomicWaker::new()); Ok(Self { addrs: Default::default(), queue: Default::default(), waker: waker.clone(), resync: resync.clone(), notif: IpChangeNotification::new(Box::new(move |_, _| { resync.store(true, Ordering::Relaxed); waker.wake(); }))?, }) } fn resync(&mut self) -> Result<()> { let addrs = if_addrs::get_if_addrs()?; for old_addr in self.addrs.clone() { if addrs .iter() .find(|addr| addr.ip() == old_addr.addr()) .is_none() { self.addrs.remove(&old_addr); self.queue.push_back(IfEvent::Down(old_addr)); } } for new_addr in addrs { let ipnet = ifaddr_to_ipnet(new_addr.addr); if self.addrs.insert(ipnet) { self.queue.push_back(IfEvent::Up(ipnet)); } } Ok(()) } /// Iterate over current networks. pub fn iter(&self) -> impl Iterator { self.addrs.iter() } /// Poll for an address change event. pub fn poll_if_event(&mut self, cx: &mut Context) -> Poll> { loop { if let Some(event) = self.queue.pop_front() { return Poll::Ready(Ok(event)); } if !self.resync.swap(false, Ordering::Relaxed) { self.waker.register(cx.waker()); return Poll::Pending; } if let Err(error) = self.resync() { return Poll::Ready(Err(error)); } } } } impl Stream for IfWatcher { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::into_inner(self).poll_if_event(cx).map(Some) } } impl FusedStream for IfWatcher { fn is_terminated(&self) -> bool { false } } fn ifaddr_to_ipnet(addr: IfAddr) -> IpNet { match addr { IfAddr::V4(ip) => { let prefix_len = (!u32::from_be_bytes(ip.netmask.octets())).leading_zeros(); IpNet::V4( Ipv4Net::new(ip.ip, prefix_len as u8).expect("if_addrs returned a valid prefix"), ) } IfAddr::V6(ip) => { let prefix_len = (!u128::from_be_bytes(ip.netmask.octets())).leading_zeros(); IpNet::V6( Ipv6Net::new(ip.ip, prefix_len as u8).expect("if_addrs returned a valid prefix"), ) } } } /// IP change notifications struct IpChangeNotification { handle: HANDLE, callback: *mut IpChangeCallback, } impl std::fmt::Debug for IpChangeNotification { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "IpChangeNotification") } } type IpChangeCallback = Box; impl IpChangeNotification { /// Register for route change notifications fn new(cb: IpChangeCallback) -> Result { unsafe extern "system" fn global_callback( caller_context: *const c_void, row: *const MIB_IPINTERFACE_ROW, notification_type: MIB_NOTIFICATION_TYPE, ) { (**(caller_context as *const IpChangeCallback))(&*row, notification_type) } let mut handle = HANDLE::default(); let callback = Box::into_raw(Box::new(cb)); unsafe { NotifyIpInterfaceChange( AF_UNSPEC, Some(global_callback), Some(callback as _), BOOLEAN(0), &mut handle as _, ) .map_err(|err| Error::new(ErrorKind::Other, err.to_string()))?; } Ok(Self { callback, handle }) } } impl Drop for IpChangeNotification { fn drop(&mut self) { unsafe { if let Err(err) = CancelMibChangeNotify2(self.handle) { log::error!("error deregistering notification: {}", err); } drop(Box::from_raw(self.callback)); } } } unsafe impl Send for IpChangeNotification {}