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: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions nmrs-gui/src/ui/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ pub fn build_header(
ctx.stack.set_visible_child_name("loading");
clear_children(&list_container);

match ctx.nm.wifi_enabled().await {
match ctx.nm.wifi_state().await.map(|s| s.enabled) {
Ok(enabled) => {
wifi_switch.set_active(enabled);
if enabled {
Expand All @@ -207,7 +207,7 @@ pub fn build_header(
glib::MainContext::default().spawn_local(async move {
clear_children(&list_container);

if let Err(err) = ctx.nm.set_wifi_enabled(sw.is_active()).await {
if let Err(err) = ctx.nm.set_wireless_enabled(sw.is_active()).await {
ctx.status.set_text(&format!("Error setting Wi-Fi: {err}"));
return;
}
Expand Down
6 changes: 6 additions & 0 deletions nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to the `nmrs` crate will be documented in this file.
## [Unreleased]
### Added
- `nmrs::agent` module: NetworkManager secret agent for credential prompting over D-Bus (`SecretAgent`, `SecretAgentBuilder`, `SecretAgentHandle`, `SecretRequest`, `SecretResponder`, `SecretSetting`, `SecretAgentFlags`, `SecretAgentCapabilities`, `CancelReason`, `SecretStoreEvent`)
- Airplane-mode surface: `RadioState`, `AirplaneModeState`, `wifi_state()`, `wwan_state()`, `bluetooth_radio_state()`, `airplane_mode_state()`, `set_wireless_enabled()`, `set_wwan_enabled()`, `set_bluetooth_radio_enabled()`, `set_airplane_mode()`
- Kernel rfkill awareness: hardware kill switch state via `/sys/class/rfkill`
- `HardwareRadioKilled` and `BluezUnavailable` error variants

### Changed
- Deprecated `wifi_enabled()`, `set_wifi_enabled()`, and `wifi_hardware_enabled()` in favor of `wifi_state()` and `set_wireless_enabled()`
- `VpnConfig` trait and `WireGuardConfig`; `NetworkManager::connect_vpn` accepts `VpnConfig` implementors; `VpnCredentials` deprecated with compatibility bridges ([#303](https://github.com/cachebag/nmrs/pull/303))

### Changed
Expand Down
6 changes: 5 additions & 1 deletion nmrs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nmrs"
version = "2.4.0"
version = "2.5.0"
authors = ["Akrm Al-Hakimi <alhakimiakrmj@gmail.com>"]
edition.workspace = true
rust-version = "1.94.0"
Expand Down Expand Up @@ -47,3 +47,7 @@ path = "examples/vpn_connect.rs"
[[example]]
name = "secret_agent"
path = "examples/secret_agent.rs"

[[example]]
name = "airplane_mode"
path = "examples/airplane_mode.rs"
1 change: 1 addition & 0 deletions nmrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Rust bindings for NetworkManager via D-Bus.
- **Profile Management**: Create, query, and delete saved connection profiles
- **Real-Time Monitoring**: Signal-based network and device state change notifications
- **Secret Agent**: Respond to NetworkManager credential prompts via an async stream API
- **Airplane Mode**: Toggle Wi-Fi, WWAN, and Bluetooth radios with rfkill hardware awareness
- **Typed Errors**: Structured error types with specific failure reasons
- **Fully Async**: Built on `zbus` with async/await throughout

Expand Down
18 changes: 18 additions & 0 deletions nmrs/examples/airplane_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;

println!("before: {:#?}", nm.airplane_mode_state().await?);

nm.set_airplane_mode(true).await?;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;

println!("during: {:#?}", nm.airplane_mode_state().await?);

nm.set_airplane_mode(false).await?;
println!("after: {:#?}", nm.airplane_mode_state().await?);

Ok(())
}
10 changes: 9 additions & 1 deletion nmrs/src/api/models/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,16 @@ pub enum ConnectionError {
/// A secret agent is already registered under this identifier.
#[error("secret agent already registered under this identifier")]
AgentAlreadyRegistered,

/// An error occured while parsing a configuration
#[error("error while parsing a configuration: {0}")]
ParseError(OvpnParseError),

/// A radio is hardware-disabled via rfkill.
#[error("radio is hardware-disabled (rfkill)")]
HardwareRadioKilled,

/// The BlueZ Bluetooth stack is unavailable.
#[error("bluetooth stack unavailable: {0}")]
BluezUnavailable(String),
}
2 changes: 2 additions & 0 deletions nmrs/src/api/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod connection_state;
mod device;
mod error;
mod openvpn;
mod radio;
mod state_reason;
mod vpn;
mod wifi;
Expand All @@ -19,6 +20,7 @@ pub use connection_state::*;
pub use device::*;
pub use error::*;
pub use openvpn::*;
pub use radio::*;
pub use state_reason::*;
pub use vpn::*;
pub use wifi::*;
Expand Down
128 changes: 128 additions & 0 deletions nmrs/src/api/models/radio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! Radio and airplane-mode state types.
//!
//! NetworkManager tracks both a software-enabled flag (controlled via D-Bus)
//! and a hardware-enabled flag (reflecting the kernel rfkill state) for each
//! radio. [`RadioState`] captures both, and [`AirplaneModeState`] aggregates
//! Wi-Fi, WWAN, and Bluetooth into a single snapshot.

/// Software and hardware enabled state for a single radio.
///
/// `enabled` reflects the user-facing toggle (can be written via D-Bus).
/// `hardware_enabled` reflects the kernel rfkill state and cannot be changed
/// from userspace — if `false`, setting `enabled = true` is accepted by NM
/// but the radio remains off until hardware is unkilled.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct RadioState {
/// Software-enabled: can the user turn this radio on via NM?
pub enabled: bool,
/// Hardware-enabled: is rfkill allowing this radio?
/// If `false`, `enabled = true` is a no-op until hardware is unkilled.
pub hardware_enabled: bool,
}

impl RadioState {
/// Creates a new `RadioState`.
#[must_use]
pub fn new(enabled: bool, hardware_enabled: bool) -> Self {
Self {
enabled,
hardware_enabled,
}
}
}

/// Aggregated radio state for all radios that `nmrs` can control.
///
/// Returned by [`NetworkManager::airplane_mode_state`](crate::NetworkManager::airplane_mode_state).
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct AirplaneModeState {
/// Wi-Fi radio state.
pub wifi: RadioState,
/// WWAN (mobile broadband) radio state.
pub wwan: RadioState,
/// Bluetooth radio state (sourced from BlueZ + rfkill).
pub bluetooth: RadioState,
}

impl AirplaneModeState {
/// Creates a new `AirplaneModeState`.
#[must_use]
pub fn new(wifi: RadioState, wwan: RadioState, bluetooth: RadioState) -> Self {
Self {
wifi,
wwan,
bluetooth,
}
}

/// Returns `true` if every radio `nmrs` can control is software-disabled.
///
/// This is the "airplane mode is on" state — all radios off.
#[must_use]
pub fn is_airplane_mode(&self) -> bool {
!self.wifi.enabled && !self.wwan.enabled && !self.bluetooth.enabled
}

/// Returns `true` if any radio has its hardware kill switch active.
#[must_use]
pub fn any_hardware_killed(&self) -> bool {
!self.wifi.hardware_enabled
|| !self.wwan.hardware_enabled
|| !self.bluetooth.hardware_enabled
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn all_off_is_airplane_mode() {
let state = AirplaneModeState::new(
RadioState::new(false, true),
RadioState::new(false, true),
RadioState::new(false, true),
);
assert!(state.is_airplane_mode());
assert!(!state.any_hardware_killed());
}

#[test]
fn any_on_is_not_airplane_mode() {
let state = AirplaneModeState::new(
RadioState::new(true, true),
RadioState::new(false, true),
RadioState::new(false, true),
);
assert!(!state.is_airplane_mode());
}

#[test]
fn hardware_killed_detected() {
let state = AirplaneModeState::new(
RadioState::new(true, true),
RadioState::new(true, false),
RadioState::new(true, true),
);
assert!(state.any_hardware_killed());
}

#[test]
fn no_hardware_kill() {
let state = AirplaneModeState::new(
RadioState::new(false, true),
RadioState::new(false, true),
RadioState::new(false, true),
);
assert!(!state.any_hardware_killed());
}

#[test]
fn radio_state_new() {
let rs = RadioState::new(true, false);
assert!(rs.enabled);
assert!(!rs.hardware_enabled);
}
}
90 changes: 81 additions & 9 deletions nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use tokio::sync::watch;
use zbus::Connection;

use crate::Result;
use crate::api::models::{Device, Network, NetworkInfo, WifiSecurity};
use crate::api::models::{
AirplaneModeState, Device, Network, NetworkInfo, RadioState, WifiSecurity,
};
use crate::core::airplane;
use crate::core::bluetooth::connect_bluetooth;
use crate::core::connection::{
connect, connect_wired, disconnect, forget_by_name_and_type, get_device_by_interface,
Expand All @@ -14,8 +17,7 @@ use crate::core::connection_settings::{
get_saved_connection_path, has_saved_connection, list_saved_connections,
};
use crate::core::device::{
is_connecting, list_bluetooth_devices, list_devices, set_wifi_enabled, wait_for_wifi_ready,
wifi_enabled, wifi_hardware_enabled,
is_connecting, list_bluetooth_devices, list_devices, wait_for_wifi_ready,
};
use crate::core::scan::{current_network, list_networks, scan_networks};
use crate::core::vpn::{connect_vpn, disconnect_vpn, get_vpn_info, list_vpn_connections};
Expand Down Expand Up @@ -89,8 +91,12 @@ use crate::types::constants::device_type;
/// let devices = nm.list_devices().await?;
///
/// // Control WiFi
/// nm.set_wifi_enabled(false).await?; // Disable WiFi
/// nm.set_wifi_enabled(true).await?; // Enable WiFi
/// nm.set_wireless_enabled(false).await?; // Disable WiFi
/// nm.set_wireless_enabled(true).await?; // Enable WiFi
///
/// // Check airplane mode
/// let state = nm.airplane_mode_state().await?;
/// println!("Airplane mode: {}", state.is_airplane_mode());
/// # Ok(())
/// # }
/// ```
Expand Down Expand Up @@ -491,20 +497,86 @@ impl NetworkManager {
get_vpn_info(&self.conn, name).await
}

/// Returns the combined software/hardware state of the Wi-Fi radio.
///
/// See [`RadioState`] for the distinction between `enabled` (software)
/// and `hardware_enabled` (rfkill).
pub async fn wifi_state(&self) -> Result<RadioState> {
airplane::wifi_state(&self.conn).await
}

/// Returns the combined software/hardware state of the WWAN radio.
pub async fn wwan_state(&self) -> Result<RadioState> {
airplane::wwan_state(&self.conn).await
}

/// Returns the combined software/hardware state of the Bluetooth radio.
///
/// Reads power state from all BlueZ adapters and cross-references rfkill.
/// If BlueZ is not running or no adapters exist, returns
/// `RadioState { enabled: true, hardware_enabled: false }`.
pub async fn bluetooth_radio_state(&self) -> Result<RadioState> {
airplane::bluetooth_radio_state(&self.conn).await
}

/// Returns the aggregated airplane-mode state across all radios.
///
/// Fans out to Wi-Fi, WWAN, and Bluetooth concurrently and returns
/// an [`AirplaneModeState`] snapshot.
pub async fn airplane_mode_state(&self) -> Result<AirplaneModeState> {
airplane::airplane_mode_state(&self.conn).await
}

/// Enables or disables the Wi-Fi radio (software toggle).
///
/// This replaces the deprecated [`set_wifi_enabled`](Self::set_wifi_enabled).
/// If the radio is hardware-killed, NM accepts the write but the radio
/// remains off until hardware is unkilled.
pub async fn set_wireless_enabled(&self, enabled: bool) -> Result<()> {
airplane::set_wireless_enabled(&self.conn, enabled).await
}

/// Enables or disables the WWAN (mobile broadband) radio.
///
/// Writes the `WwanEnabled` property on NetworkManager.
pub async fn set_wwan_enabled(&self, enabled: bool) -> Result<()> {
airplane::set_wwan_enabled(&self.conn, enabled).await
}

/// Enables or disables the Bluetooth radio by toggling all BlueZ adapters.
///
/// Returns [`BluezUnavailable`](crate::ConnectionError::BluezUnavailable) if BlueZ is not running
/// or no adapters exist.
pub async fn set_bluetooth_radio_enabled(&self, enabled: bool) -> Result<()> {
airplane::set_bluetooth_radio_enabled(&self.conn, enabled).await
}

/// Flips all three radios in one call.
///
/// **`enabled = true` means airplane mode is on, i.e. radios are off.**
///
/// Does not fail fast: attempts all three toggles concurrently and
/// returns the first error at the end, if any.
pub async fn set_airplane_mode(&self, enabled: bool) -> Result<()> {
airplane::set_airplane_mode(&self.conn, enabled).await
}

/// Returns whether Wi-Fi is currently enabled.
#[deprecated(since = "2.5.0", note = "use `wifi_state()` instead")]
pub async fn wifi_enabled(&self) -> Result<bool> {
wifi_enabled(&self.conn).await
Ok(self.wifi_state().await?.enabled)
}

/// Enables or disables Wi-Fi.
#[deprecated(since = "2.5.0", note = "use `set_wireless_enabled()` instead")]
pub async fn set_wifi_enabled(&self, value: bool) -> Result<()> {
set_wifi_enabled(&self.conn, value).await
self.set_wireless_enabled(value).await
}

/// Returns whether wireless hardware is currently enabled.
/// Reflects rfkill state which helps check if the radio is enabled or blocked.
#[deprecated(since = "2.5.0", note = "use `wifi_state()` instead")]
pub async fn wifi_hardware_enabled(&self) -> Result<bool> {
wifi_hardware_enabled(&self.conn).await
Ok(self.wifi_state().await?.hardware_enabled)
}

/// Waits for a Wi-Fi device to become ready (disconnected or activated).
Expand Down
Loading