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
2 changes: 2 additions & 0 deletions nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 109 additions & 7 deletions nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<bool> {
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<Option<Network>> {
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<Vec<String>> {
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<zvariant::OwnedObjectPath> {
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<String> {
Expand Down Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions nmrs/src/core/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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<OwnedObjectPath> {
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)
}
31 changes: 31 additions & 0 deletions nmrs/src/core/connection_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<String>> {
let settings = settings_proxy(conn).await?;

let reply = settings.call_method("ListConnections", &()).await?;
let conns: Vec<OwnedObjectPath> = 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::<HashMap<String, HashMap<String, Value>>>() {
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)
}
80 changes: 78 additions & 2 deletions nmrs/src/core/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -94,3 +95,78 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result<Vec<Network>> {

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<Option<Network>> {
// 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)
}
Loading