Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ All notable changes to the `nmrs` crate will be documented in this file.
- Support for specifying Bluetooth adapter in `BluetoothIdentity` ([#267](https://github.com/cachebag/nmrs/pull/267))

### Fixed
- `monitor_network_changes` now fires for Wi-Fi access point signal strength changes, not only access point additions and removals ([#363](https://github.com/cachebag/nmrs/issues/363))
- `list_networks` fills `device`, `ip4_address`, and `ip6_address` for the access point currently in use on each Wi-Fi interface ([#368](https://github.com/cachebag/nmrs/pull/368))
- `monitor_network_changes` now fires for Wi-Fi access point signal strength changes, not only access point additions and removals ([#367](https://github.com/cachebag/nmrs/pull/367))
- Add `Send` bound to monitoring stream trait objects so `monitor_network_changes` and `monitor_device_changes` work with `tokio::spawn` ([#359](https://github.com/cachebag/nmrs/pull/359))
- Line-accurate source locations for `.ovpn` directives and blocks ([#318](https://github.com/cachebag/nmrs/pull/318))
- `key_direction` when nested under `tls_auth` and as a standalone directive ([#320](https://github.com/cachebag/nmrs/pull/320))
Expand Down
51 changes: 51 additions & 0 deletions nmrs/src/api/models/wifi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,5 +628,56 @@ impl Network {
self.is_psk |= other.is_psk;
self.is_eap |= other.is_eap;
self.is_hotspot |= other.is_hotspot;

if self.ip4_address.is_none() {
self.ip4_address.clone_from(&other.ip4_address);
}
if self.ip6_address.is_none() {
self.ip6_address.clone_from(&other.ip6_address);
}
if self.device.is_empty() {
self.device.clone_from(&other.device);
}
}
}

#[cfg(test)]
mod network_merge_tests {
use super::Network;

#[test]
fn merge_ap_keeps_ip_and_device_when_stronger_ap_has_none() {
let mut weaker_connected = Network {
device: "wlan0".into(),
ssid: "net".into(),
bssid: Some("aa:aa:aa:aa:aa:aa".into()),
strength: Some(20),
frequency: Some(5200),
secured: true,
is_psk: true,
is_eap: false,
is_hotspot: false,
ip4_address: Some("192.168.1.5/24".into()),
ip6_address: Some("fe80::1/64".into()),
};
let stronger = Network {
device: String::new(),
ssid: "net".into(),
bssid: Some("bb:bb:bb:bb:bb:bb".into()),
strength: Some(90),
frequency: Some(5200),
secured: true,
is_psk: true,
is_eap: false,
is_hotspot: false,
ip4_address: None,
ip6_address: None,
};
weaker_connected.merge_ap(&stronger);
assert_eq!(weaker_connected.strength, Some(90));
assert_eq!(weaker_connected.bssid, Some("bb:bb:bb:bb:bb:bb".into()));
assert_eq!(weaker_connected.ip4_address, Some("192.168.1.5/24".into()));
assert_eq!(weaker_connected.ip6_address, Some("fe80::1/64".into()));
assert_eq!(weaker_connected.device, "wlan0");
}
}
19 changes: 15 additions & 4 deletions nmrs/src/core/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@ pub(crate) async fn scan_networks(conn: &Connection) -> Result<()> {
///
/// When multiple access points share the same SSID and frequency (e.g., mesh
/// networks), the one with the strongest signal is returned.
///
/// For the access point that matches the device's active connection, the result
/// includes the interface name and assigned IPv4/IPv6 addresses (CIDR), consistent
/// with `current_network`.
pub(crate) async fn list_networks(conn: &Connection) -> Result<Vec<Network>> {
let mut networks: HashMap<(String, u32), Network> = HashMap::new();

let all_networks = for_each_access_point(conn, |ap| {
let all_networks = for_each_access_point(conn, |_dev, active_ap, ap_path, ap, on_device| {
let is_this_ap = active_ap.as_str() != "/" && active_ap == &ap_path;
Box::pin(async move {
let ssid_bytes = ap.ssid().await?;
let ssid = decode_ssid_or_hidden(&ssid_bytes);
Expand All @@ -89,8 +94,14 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result<Vec<Network>> {
let is_eap = (wpa & security_flags::EAP) != 0 || (rsn & security_flags::EAP) != 0;
let is_hotspot = ap.mode().await.unwrap_or(0) == wifi_mode::AP;

let (device, ip4_address, ip6_address) = if is_this_ap {
on_device
} else {
(String::new(), None, None)
};

let network = Network {
device: String::new(),
device,
ssid: ssid.to_string(),
bssid: Some(bssid),
strength: Some(strength),
Expand All @@ -99,8 +110,8 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result<Vec<Network>> {
is_psk,
is_eap,
is_hotspot,
ip4_address: None,
ip6_address: None,
ip4_address,
ip6_address,
};

Ok(Some((ssid, frequency, network)))
Expand Down
2 changes: 1 addition & 1 deletion nmrs/src/monitoring/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub(crate) async fn show_details(conn: &Connection, net: &Network) -> Result<Net
(None, None)
};

let results = for_each_access_point(conn, |ap| {
let results = for_each_access_point(conn, |_dev, _active_ap, _ap_path, ap, _on_device| {
let target_ssid = target_ssid_outer.clone();
let is_connected = is_connected_outer;
Box::pin(async move {
Expand Down
30 changes: 27 additions & 3 deletions nmrs/src/util/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ pub(crate) fn strength_or_zero(strength: Option<u8>) -> u8 {

/// This helper iterates through all WiFi access points and calls the provided async function.
///
/// Loops through devices, filters for WiFi, and invokes `func` with each access point proxy.
/// Loops through devices, filters for WiFi, and invokes `func` for each access point.
///
/// For each Wi-Fi device, queries the active access point and, when connected, the interface
/// name and IP addresses once per device (not per AP).
///
/// The function is awaited immediately in the loop to avoid lifetime issues.
///
/// The `+ Send` bound on the returned future lets callers await this helper (and everything
Expand All @@ -115,7 +119,11 @@ pub(crate) fn strength_or_zero(strength: Option<u8>) -> u8 {
pub(crate) async fn for_each_access_point<F, T>(conn: &Connection, mut func: F) -> Result<Vec<T>>
where
F: for<'a> FnMut(
&'a NMDeviceProxy<'a>,
&'a OwnedObjectPath,
OwnedObjectPath,
&'a NMAccessPointProxy<'a>,
(String, Option<String>, Option<String>),
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Option<T>>> + Send + 'a>,
>,
Expand All @@ -140,12 +148,28 @@ where
.build()
.await?;

let active_ap = wifi.active_access_point().await?;
let on_device = if active_ap.as_str() != "/" {
match d_proxy.active_connection().await {
Ok(ac) if ac.as_str() != "/" => {
let (ip4, ip6) = get_ip_addresses_from_active_connection(conn, &ac).await;
let iface = d_proxy.interface().await.unwrap_or_default();
(iface, ip4, ip6)
}
_ => (String::new(), None, None),
}
} else {
(String::new(), None, None)
};

for ap_path in wifi.access_points().await? {
let ap = NMAccessPointProxy::builder(conn)
.path(ap_path)?
.path(ap_path.clone())?
.build()
.await?;
if let Some(result) = func(&ap).await? {
if let Some(result) =
func(&d_proxy, &active_ap, ap_path, &ap, on_device.clone()).await?
{
results.push(result);
}
}
Expand Down