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
7 changes: 4 additions & 3 deletions nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ All notable changes to the `nmrs` crate will be documented in this file.
- `HardwareRadioKilled` and `BluezUnavailable` error variants
- Per-Wi-Fi-device scoping: `WifiDevice` model, `list_wifi_devices()`, `wifi_device_by_interface()`, `WifiScope` builder via `nm.wifi("wlan1")`, `set_wifi_enabled(interface, bool)` for per-radio enable/disable
- `WifiInterfaceNotFound` and `NotAWifiDevice` error variants
- Saved profile enumeration: `SavedConnection`, `SavedConnectionBrief`, `SettingsSummary`, `SettingsPatch`, `WifiSecuritySummary`, `WifiKeyMgmt`, `VpnSecretFlags`; `list_saved_connections()`, `list_saved_connections_brief()`, `list_saved_connection_ids()`, `get_saved_connection()`, `get_saved_connection_raw()`, `delete_saved_connection()`, `update_saved_connection()`, `reload_saved_connections()`; D-Bus proxies `NMSettingsProxy` / `NMSettingsConnectionProxy`; example `saved_list`

### Changed
- **Breaking (3.0):** `connect`, `connect_to_bssid`, `disconnect`, `scan_networks`, and `list_networks` now take an `interface: Option<&str>` parameter. Pass `None` to preserve previous behavior, or `Some("wlan1")` to scope to a specific Wi-Fi interface. For an ergonomic per-interface API, use `nm.wifi("wlan1")` to obtain a `WifiScope`.
- **Breaking (3.0):** `set_wifi_enabled` now requires an `interface: &str` argument and toggles only that radio (via `Device.Autoconnect` + `Device.Disconnect()`). For the global wireless killswitch use `set_wireless_enabled(bool)`.
- **Breaking (3.0):** Removed deprecated `wifi_enabled()`, `wifi_hardware_enabled()`, and the no-arg `set_wifi_enabled(bool)`. Use `wifi_state()` and `set_wireless_enabled()`.
-`list_saved_connections()` now returns `Vec<SavedConnection>` (full decode + summaries). Use `list_saved_connection_ids()` for the previous `Vec<String>` behavior (connection `id` names only).
-`connect`, `connect_to_bssid`, `disconnect`, `scan_networks`, and `list_networks` now take an `interface: Option<&str>` parameter. Pass `None` to preserve previous behavior, or `Some("wlan1")` to scope to a specific Wi-Fi interface. For an ergonomic per-interface API, use `nm.wifi("wlan1")` to obtain a `WifiScope`.
-`set_wifi_enabled` now requires an `interface: &str` argument and toggles only that radio (via `Device.Autoconnect` + `Device.Disconnect()`). For the global wireless killswitch use `set_wireless_enabled(bool)`.
- `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
4 changes: 4 additions & 0 deletions nmrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ path = "examples/ap_list.rs"
[[example]]
name = "multi_wifi"
path = "examples/multi_wifi.rs"

[[example]]
name = "saved_list"
path = "examples/saved_list.rs"
2 changes: 1 addition & 1 deletion nmrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Rust bindings for NetworkManager via D-Bus.
- **Ethernet**: Wired network connection management
- **Network Discovery**: Scan and list available access points with per-BSSID detail and security capabilities
- **Per-Interface Scoping**: Target specific Wi-Fi radios on multi-NIC systems via `nm.wifi("wlan1")` or `Option<&str>` interface arguments
- **Profile Management**: Create, query, and delete saved connection profiles
- **Profile Management**: List saved profiles with decoded summaries (`list_saved_connections`), raw settings, UUID-based delete/update, plus create/query/delete helpers
- **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
Expand Down
74 changes: 74 additions & 0 deletions nmrs/examples/saved_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! List all saved NetworkManager connection profiles with decoded summaries.
//!
//! Run: `cargo run --example saved_list`
//!
//! Secrets (Wi-Fi PSK, VPN passwords, etc.) are not shown — only non-secret
//! settings returned by `GetSettings`.

use nmrs::{NetworkManager, SettingsSummary};

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

let mut profiles = nm.list_saved_connections().await?;
profiles.sort_by(|a, b| a.id.cmp(&b.id));

for c in profiles {
print!("{:<32} {:<22} {}", c.id, c.connection_type, c.uuid);
if !c.autoconnect {
print!(" [manual]");
}
if c.unsaved {
print!(" [unsaved]");
}
println!();

match &c.summary {
SettingsSummary::Wifi {
ssid,
security,
hidden,
..
} => {
println!(" ssid={ssid:?} hidden={hidden} security={security:?}");
}
SettingsSummary::Vpn {
service_type,
user_name,
data_keys,
..
} => {
println!(" vpn={service_type} user={user_name:?} data_keys={data_keys:?}");
}
SettingsSummary::WireGuard {
peer_count,
first_peer_endpoint,
listen_port,
..
} => {
println!(
" wireguard listen={listen_port:?} peers={peer_count} endpoint={first_peer_endpoint:?}"
);
}
SettingsSummary::Ethernet { mac_address, .. } => {
println!(" ethernet mac={mac_address:?}");
}
SettingsSummary::Gsm { apn, .. } => {
println!(" gsm apn={apn:?}");
}
SettingsSummary::Bluetooth { bdaddr, bt_type } => {
println!(" bluetooth {bdaddr} type={bt_type}");
}
SettingsSummary::Cdma { number, .. } => {
println!(" cdma number={number:?}");
}
SettingsSummary::Other { sections } => {
println!(" other sections={sections:?}");
}
_ => println!(" (additional summary variant)"),
}
}

Ok(())
}
8 changes: 8 additions & 0 deletions nmrs/src/api/models/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ pub enum ConnectionError {
#[error("no saved connection for network")]
NoSavedConnection,

/// No saved profile with the given UUID.
#[error("saved connection '{0}' not found")]
SavedConnectionNotFound(String),

/// Saved profile settings are missing required keys or are inconsistent.
#[error("saved connection malformed: {0}")]
MalformedSavedConnection(String),

/// An empty password was provided for the requested network.
#[error("no password was provided")]
MissingPassword,
Expand Down
2 changes: 2 additions & 0 deletions nmrs/src/api/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod device;
mod error;
mod openvpn;
mod radio;
mod saved_connection;
mod state_reason;
mod vpn;
mod wifi;
Expand All @@ -23,6 +24,7 @@ pub use device::*;
pub use error::*;
pub use openvpn::*;
pub use radio::*;
pub use saved_connection::*;
pub use state_reason::*;
pub use vpn::*;
pub use wifi::*;
Expand Down
216 changes: 216 additions & 0 deletions nmrs/src/api/models/saved_connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//! Saved NetworkManager connection profiles with decoded settings summaries.
//!
//! Use [`crate::NetworkManager::list_saved_connections`] to enumerate every
//! profile NM knows about (Wi-Fi, Ethernet, VPN, WireGuard, mobile, Bluetooth).
//! Secrets (PSK, EAP passwords, VPN tokens) are **not** included in
//! [`SavedConnection`] — NetworkManager only returns them via
//! [`GetSecrets`](https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html#gdbus-method-org-freedesktop-NetworkManager-Settings-Connection.GetSecrets)
//! when a [secret agent](crate::agent) is registered. See feature `01-secret-agent`.

use std::collections::HashMap;

use zvariant::{OwnedObjectPath, OwnedValue};

/// Full saved profile with a structured [`SettingsSummary`].
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct SavedConnection {
/// D-Bus object path of the settings connection.
pub path: OwnedObjectPath,
/// Connection UUID (`connection.uuid`).
pub uuid: String,
/// Human-visible name (`connection.id`).
pub id: String,
/// NM connection type string (`connection.type`), e.g. `802-11-wireless`.
pub connection_type: String,
/// Bound interface, if any (`connection.interface-name`).
pub interface_name: Option<String>,
/// Whether NM may auto-activate this profile (`connection.autoconnect`).
pub autoconnect: bool,
/// Autoconnect priority (`connection.autoconnect-priority`).
pub autoconnect_priority: i32,
/// Last activation time as Unix seconds (`connection.timestamp`), or `0` if never.
pub timestamp_unix: u64,
/// `connection.permissions` user strings, if present.
pub permissions: Vec<String>,
/// In-memory-only profile not yet written to disk.
pub unsaved: bool,
/// On-disk keyfile path when saved.
pub filename: Option<String>,
/// Decoded type-specific fields (no secrets).
pub summary: SettingsSummary,
}

/// Cheap listing: path plus `connection` identity fields only (still one `GetSettings` per profile).
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct SavedConnectionBrief {
/// D-Bus object path.
pub path: OwnedObjectPath,
/// `connection.uuid`.
pub uuid: String,
/// `connection.id`.
pub id: String,
/// `connection.type`.
pub connection_type: String,
}

/// Partial update merged via [`crate::NetworkManager::update_saved_connection`].
#[non_exhaustive]
#[derive(Debug, Default, Clone)]
pub struct SettingsPatch {
/// When `Some`, sets `connection.autoconnect`.
pub autoconnect: Option<bool>,
/// When `Some`, sets `connection.autoconnect-priority`.
pub autoconnect_priority: Option<i32>,
/// When `Some`, sets `connection.id`.
pub id: Option<String>,
/// `Some(Some(name))` sets `interface-name`; `Some(None)` clears it (best-effort empty string).
pub interface_name: Option<Option<String>>,
/// Merged after the fields above; section → key → value. Overwrites keys present.
pub raw_overlay: Option<HashMap<String, HashMap<String, OwnedValue>>>,
}

/// NM `password-flags` / `psk-flags` style bitmask (subset used for summaries).
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct VpnSecretFlags(pub u32);

impl VpnSecretFlags {
/// `NM_SETTING_SECRET_FLAG_AGENT_OWNED`.
pub const AGENT_OWNED: u32 = 0x1;

/// True if the secret is expected to be provided by an agent.
#[must_use]
pub fn agent_owned(self) -> bool {
self.0 & Self::AGENT_OWNED != 0
}
}

/// Wi-Fi key management style from `802-11-wireless-security.key-mgmt`.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WifiKeyMgmt {
/// Open or no key management string.
None,
/// WEP (legacy).
Wep,
/// WPA-PSK (`wpa-psk`, `wpa-none`, …).
WpaPsk,
/// WPA-EAP / 802.1X.
WpaEap,
/// SAE (WPA3-Personal).
Sae,
/// OWE.
Owe,
/// OWE transition mode.
OweTransitionMode,
}

/// Non-secret Wi-Fi security hints for UI / filtering.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WifiSecuritySummary {
/// Derived key management style.
pub key_mgmt: WifiKeyMgmt,
/// `psk` key exists in non-secret settings.
pub has_psk_field: bool,
/// `psk-flags` has [`VpnSecretFlags::AGENT_OWNED`].
pub psk_agent_owned: bool,
/// EAP method names from `802-1x.eap`.
pub eap_methods: Vec<String>,
}

/// Decoded summary for the connection `type` (and related sections).
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum SettingsSummary {
/// `802-11-wireless` — SSID and security hints (no PSK / EAP secrets).
Wifi {
/// Decoded SSID (hidden networks may be empty).
ssid: String,
/// `mode` string from settings: `infrastructure`, `ap`, `adhoc`, …
mode: Option<String>,
/// Present when a security block exists (`802-11-wireless-security` / `802-1x`).
security: Option<WifiSecuritySummary>,
/// `band` if set (`a` / `bg`).
band: Option<String>,
/// `channel` if set.
channel: Option<u32>,
/// `bssid` MAC string if set.
bssid: Option<String>,
/// `hidden` property.
hidden: bool,
/// `mac-address-randomization` if set.
mac_randomization: Option<String>,
},
/// `802-3-ethernet`.
Ethernet {
/// `mac-address` string if set.
mac_address: Option<String>,
/// `auto-negotiate`.
auto_negotiate: Option<bool>,
/// `speed` in Mbps.
speed_mbps: Option<u32>,
/// `mtu`.
mtu: Option<u32>,
},
/// Generic `vpn` connection (non-WireGuard service types).
Vpn {
/// `vpn.service-type` (e.g. OpenVPN plugin name).
service_type: String,
/// `vpn.user-name`.
user_name: Option<String>,
/// `vpn.password-flags`.
password_flags: VpnSecretFlags,
/// Keys present in `vpn.data` (values omitted).
data_keys: Vec<String>,
/// `vpn.persistent` when present.
persistent: bool,
},
/// Native WireGuard or VPN plugin pointing at WireGuard.
WireGuard {
/// `listen-port`.
listen_port: Option<u16>,
/// `mtu`.
mtu: Option<u32>,
/// `fwmark`.
fwmark: Option<u32>,
/// Number of peer dicts under `wireguard.peers`.
peer_count: usize,
/// `endpoint` of the first peer, if any.
first_peer_endpoint: Option<String>,
},
/// `gsm` mobile broadband.
Gsm {
/// `apn`.
apn: Option<String>,
/// `username`.
user_name: Option<String>,
/// `password-flags`.
password_flags: u32,
/// `pin-flags`.
pin_flags: u32,
},
/// `cdma` mobile broadband.
Cdma {
/// `number`.
number: Option<String>,
/// `username`.
user_name: Option<String>,
/// `password-flags`.
password_flags: u32,
},
/// `bluetooth`.
Bluetooth {
/// Bluetooth MAC / bdaddr.
bdaddr: String,
/// `type` (`panu`, `dun`, …).
bt_type: String,
},
/// Any other `connection.type` — lists settings section names only.
Other {
/// Keys from the top-level settings dict (`connection`, `ipv4`, …).
sections: Vec<String>,
},
}
Loading