diff --git a/nmrs/CHANGELOG.md b/nmrs/CHANGELOG.md index cebbffcd..e221aeef 100644 --- a/nmrs/CHANGELOG.md +++ b/nmrs/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to the `nmrs` crate will be documented in this file. ## [Unreleased] ### Added - Input validation before any D-Bus operations ([#173](https://github.com/cachebag/nmrs/pull/173)) +- CI: adjust workflow to auto-update nix hashes on PRs ([#182](https://github.com/cachebag/nmrs/pull/182)) +- More helpful methods to `network_manager` facade ([#190](https://github.com/cachebag/nmrs/pull/190)) ## [1.3.5] - 2026-01-13 ### Changed diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index fbe09620..726ae08a 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -1,10 +1,14 @@ use zbus::Connection; use crate::api::models::{Device, Network, NetworkInfo, WifiSecurity}; -use crate::core::connection::{connect, connect_wired, forget}; -use crate::core::connection_settings::{get_saved_connection_path, has_saved_connection}; +use crate::core::connection::{ + connect, connect_wired, disconnect, forget, get_device_by_interface, is_connected, +}; +use crate::core::connection_settings::{ + get_saved_connection_path, has_saved_connection, list_saved_connections, +}; use crate::core::device::{list_devices, set_wifi_enabled, wait_for_wifi_ready, wifi_enabled}; -use crate::core::scan::{list_networks, scan_networks}; +use crate::core::scan::{current_network, list_networks, scan_networks}; use crate::core::vpn::{connect_vpn, disconnect_vpn, get_vpn_info, list_vpn_connections}; use crate::models::{VpnConnection, VpnConnectionInfo, VpnCredentials}; use crate::monitoring::device as device_monitor; @@ -331,6 +335,100 @@ impl NetworkManager { scan_networks(&self.conn).await } + /// Check if a network is connected + pub async fn is_connected(&self, ssid: &str) -> Result { + is_connected(&self.conn, ssid).await + } + + /// Disconnects from the current network. + /// + /// If currently connected to a WiFi network, this will deactivate + /// the connection and wait for the device to reach disconnected state. + /// + /// Returns `Ok(())` if disconnected successfully or if no active connection exists. + /// + /// # Example + /// + /// ```no_run + /// use nmrs::NetworkManager; + /// + /// # async fn example() -> nmrs::Result<()> { + /// let nm = NetworkManager::new().await?; + /// nm.disconnect().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn disconnect(&self) -> Result<()> { + disconnect(&self.conn).await + } + + /// Returns the full `Network` object for the currently connected WiFi network. + /// + /// This provides detailed information about the active connection including + /// signal strength, frequency, security type, and BSSID. + /// + /// # Example + /// + /// ```no_run + /// use nmrs::NetworkManager; + /// + /// # async fn example() -> nmrs::Result<()> { + /// let nm = NetworkManager::new().await?; + /// if let Some(network) = nm.current_network().await? { + /// println!("Connected to: {} ({}%)", network.ssid, network.strength.unwrap_or(0)); + /// } else { + /// println!("Not connected"); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn current_network(&self) -> Result> { + current_network(&self.conn).await + } + + /// Lists all saved connection profiles. + /// + /// Returns the names (IDs) of all saved connection profiles in NetworkManager, + /// including WiFi, Ethernet, VPN, and other connection types. + /// + /// # Example + /// + /// ```no_run + /// use nmrs::NetworkManager; + /// + /// # async fn example() -> nmrs::Result<()> { + /// let nm = NetworkManager::new().await?; + /// let connections = nm.list_saved_connections().await?; + /// for name in connections { + /// println!("Saved connection: {}", name); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn list_saved_connections(&self) -> Result> { + list_saved_connections(&self.conn).await + } + + /// Finds a device by its interface name (e.g., "wlan0", "eth0"). + /// + /// Returns the D-Bus object path of the device if found. + /// + /// # Example + /// + /// ```no_run + /// use nmrs::NetworkManager; + /// + /// # async fn example() -> nmrs::Result<()> { + /// let nm = NetworkManager::new().await?; + /// let device_path = nm.get_device_by_interface("wlan0").await?; + /// println!("Device path: {}", device_path.as_str()); + /// # Ok(()) + /// # } + /// ``` + pub async fn get_device_by_interface(&self, name: &str) -> Result { + get_device_by_interface(&self.conn, name).await + } + /// Returns the SSID of the currently connected network, if any. #[must_use] pub async fn current_ssid(&self) -> Option { @@ -361,14 +459,18 @@ impl NetworkManager { get_saved_connection_path(&self.conn, ssid).await } - /// Forgets (deletes) a saved connection for the given SSID. + /// Forgets (deletes) a saved WiFi connection for the given SSID. + /// + /// If currently connected to this network, disconnects first, then deletes + /// all saved connection profiles matching the SSID. /// - /// If currently connected to this network, disconnects first. + /// # Returns + /// + /// Returns `Ok(())` if at least one connection was deleted successfully. + /// Returns `NoSavedConnection` if no matching connections were found. pub async fn forget(&self, ssid: &str) -> Result<()> { forget(&self.conn, ssid).await } - - /// Monitors Wi-Fi network changes in real-time. /// /// Subscribes to D-Bus signals for access point additions and removals /// on all Wi-Fi devices. Invokes the callback whenever the network list diff --git a/nmrs/src/core/connection.rs b/nmrs/src/core/connection.rs index 4e2be988..3190c20d 100644 --- a/nmrs/src/core/connection.rs +++ b/nmrs/src/core/connection.rs @@ -650,3 +650,91 @@ fn decide_saved_connection( None => Ok(SavedDecision::RebuildFresh), } } + +/// Checks if currently connected to the specified SSID. +/// +/// If already connected, returns true. Otherwise, returns false. +/// This can be used to skip redundant connection attempts. +pub(crate) async fn is_connected(conn: &Connection, ssid: &str) -> Result { + if let Some(active) = current_ssid(conn).await { + debug!("Currently connected to: {active}"); + if active == ssid { + debug!("Already connected to {active}"); + return Ok(true); + } + } else { + debug!("Not currently connected to any network"); + } + Ok(false) +} + +/// Disconnects from the currently active network. +/// +/// This finds the current active WiFi connection and deactivates it, +/// then waits for the device to reach disconnected state. +/// +/// Returns `Ok(())` if disconnected successfully or if no active connection exists. +pub(crate) async fn disconnect(conn: &Connection) -> Result<()> { + let nm = NMProxy::new(conn).await?; + + let wifi_device = match find_wifi_device(conn, &nm).await { + Ok(dev) => dev, + Err(ConnectionError::NoWifiDevice) => { + debug!("No WiFi device found"); + return Ok(()); + } + Err(e) => return Err(e), + }; + + let dev = NMDeviceProxy::builder(conn) + .path(wifi_device.clone())? + .build() + .await?; + + let current_state = dev.state().await?; + if current_state == device_state::DISCONNECTED || current_state == device_state::UNAVAILABLE { + debug!("Device already disconnected"); + return Ok(()); + } + + if let Ok(conns) = nm.active_connections().await { + for conn_path in conns { + match nm.deactivate_connection(conn_path.clone()).await { + Ok(_) => debug!("Connection deactivated"), + Err(e) => warn!("Failed to deactivate connection: {}", e), + } + } + } + + disconnect_wifi_and_wait(conn, &wifi_device).await?; + + info!("Disconnected from network"); + Ok(()) +} + +/// Finds a device by its interface name. +/// +/// Returns the device path if found, or an error if not found. +pub(crate) async fn get_device_by_interface( + conn: &Connection, + interface_name: &str, +) -> Result { + let nm = NMProxy::new(conn).await?; + let devices = nm.get_devices().await?; + + for dev_path in devices { + let dev = NMDeviceProxy::builder(conn) + .path(dev_path.clone())? + .build() + .await?; + + if let Ok(iface) = dev.interface().await { + if iface == interface_name { + debug!("Found device with interface: {}", interface_name); + return Ok(dev_path); + } + } + } + + Err(ConnectionError::NotFound) +} diff --git a/nmrs/src/core/connection_settings.rs b/nmrs/src/core/connection_settings.rs index b94899ab..0ea46fcd 100644 --- a/nmrs/src/core/connection_settings.rs +++ b/nmrs/src/core/connection_settings.rs @@ -79,3 +79,34 @@ pub(crate) async fn delete_connection(conn: &Connection, conn_path: OwnedObjectP debug!("Deleted connection: {}", conn_path.as_str()); Ok(()) } + +/// Lists all saved connection profiles. +/// +/// Returns a vector of connection names (IDs) for all saved profiles +/// in NetworkManager. This includes WiFi, Ethernet, VPN, and other connection types. +pub(crate) async fn list_saved_connections(conn: &Connection) -> Result> { + let settings = settings_proxy(conn).await?; + + let reply = settings.call_method("ListConnections", &()).await?; + let conns: Vec = reply.body().deserialize()?; + + let mut connection_names = Vec::new(); + + for cpath in conns { + let cproxy = connection_settings_proxy(conn, cpath.clone()).await?; + + if let Ok(msg) = cproxy.call_method("GetSettings", &()).await { + let body = msg.body(); + if let Ok(all) = body.deserialize::>>() { + if let Some(conn_section) = all.get("connection") { + if let Some(Value::Str(id)) = conn_section.get("id") { + connection_names.push(id.to_string()); + } + } + } + } + } + + debug!("Found {} saved connection(s)", connection_names.len()); + Ok(connection_names) +} diff --git a/nmrs/src/core/scan.rs b/nmrs/src/core/scan.rs index 6c1509f9..b630dce3 100644 --- a/nmrs/src/core/scan.rs +++ b/nmrs/src/core/scan.rs @@ -7,9 +7,10 @@ use std::collections::HashMap; use zbus::Connection; use crate::api::models::Network; -use crate::dbus::{NMDeviceProxy, NMProxy, NMWirelessProxy}; +use crate::dbus::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; +use crate::monitoring::info::current_ssid; use crate::types::constants::{device_type, security_flags}; -use crate::util::utils::{decode_ssid_or_hidden, for_each_access_point}; +use crate::util::utils::{decode_ssid_or_empty, decode_ssid_or_hidden, for_each_access_point}; use crate::Result; /// Triggers a Wi-Fi scan on all wireless devices. @@ -94,3 +95,78 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result> { Ok(networks.into_values().collect()) } + +/// Returns the full Network object for the currently connected WiFi network. +/// +/// Returns `None` if not connected to any WiFi network. +pub(crate) async fn current_network(conn: &Connection) -> Result> { + // Get current SSID + let current_ssid = match current_ssid(conn).await { + Some(ssid) => ssid, + None => return Ok(None), + }; + + // Find the WiFi device and active access point + let nm = NMProxy::new(conn).await?; + let devices = nm.get_devices().await?; + + for dev_path in devices { + let dev = NMDeviceProxy::builder(conn) + .path(dev_path.clone())? + .build() + .await?; + + if dev.device_type().await? != device_type::WIFI { + continue; + } + + let wifi = NMWirelessProxy::builder(conn) + .path(dev_path.clone())? + .build() + .await?; + + let ap_path = wifi.active_access_point().await?; + if ap_path.as_str() == "/" { + continue; + } + + let ap = NMAccessPointProxy::builder(conn) + .path(ap_path)? + .build() + .await?; + + let ssid_bytes = ap.ssid().await?; + let ssid = decode_ssid_or_empty(&ssid_bytes); + + if ssid != current_ssid { + continue; + } + + // Found the active AP, build Network object + let strength = ap.strength().await?; + let bssid = ap.hw_address().await?; + let flags = ap.flags().await?; + let wpa = ap.wpa_flags().await?; + let rsn = ap.rsn_flags().await?; + let frequency = ap.frequency().await?; + + let secured = (flags & security_flags::WEP) != 0 || wpa != 0 || rsn != 0; + let is_psk = (wpa & security_flags::PSK) != 0 || (rsn & security_flags::PSK) != 0; + let is_eap = (wpa & security_flags::EAP) != 0 || (rsn & security_flags::EAP) != 0; + + let interface = dev.interface().await.unwrap_or_default(); + + return Ok(Some(Network { + device: interface, + ssid: ssid.to_string(), + bssid: Some(bssid), + strength: Some(strength), + frequency: Some(frequency), + secured, + is_psk, + is_eap, + })); + } + + Ok(None) +}