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.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ uuid = { version = "1.23.1", features = ["v4", "v5"] }
futures = "0.3.32"
futures-timer = "3.0.3"
base64 = "0.22.1"
nmrs = { path = "nmrs", version = "2.2" }
nmrs = { path = "nmrs", version = "3.0" }
gtk = { version = "0.11.2", package = "gtk4" }
glib = "0.22.5"
dirs = "6.0.0"
Expand Down
2 changes: 1 addition & 1 deletion nmrs-gui/src/ui/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ fn draw_connect_modal(
};

debug!("Calling nm.connect() for '{ssid}'");
match nm.connect(&ssid, creds).await {
match nm.connect(&ssid, None, creds).await {
Ok(_) => {
debug!("nm.connect() succeeded!");
status.set_text("✓ Connected!");
Expand Down
8 changes: 4 additions & 4 deletions nmrs-gui/src/ui/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,23 +310,23 @@ pub async fn refresh_networks(
wireless_header.set_margin_start(12);
list_container.append(&wireless_header);

if let Err(err) = ctx.nm.scan_networks().await {
if let Err(err) = ctx.nm.scan_networks(None).await {
ctx.status.set_text(&format!("Scan failed: {err}"));
is_scanning.set(false);
return;
}

let mut last_len = 0;
for _ in 0..5 {
let nets = ctx.nm.list_networks().await.unwrap_or_default();
let nets = ctx.nm.list_networks(None).await.unwrap_or_default();
if nets.len() == last_len && last_len > 0 {
break;
}
last_len = nets.len();
glib::timeout_future_seconds(1).await;
}

match ctx.nm.list_networks().await {
match ctx.nm.list_networks(None).await {
Ok(mut nets) => {
let current_conn = ctx.nm.current_connection_info().await;
let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
Expand Down Expand Up @@ -460,7 +460,7 @@ pub async fn refresh_networks_no_scan(
wireless_header.set_margin_start(12);
list_container.append(&wireless_header);

match ctx.nm.list_networks().await {
match ctx.nm.list_networks(None).await {
Ok(mut nets) => {
let current_conn = ctx.nm.current_connection_info().await;
let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
Expand Down
4 changes: 2 additions & 2 deletions nmrs-gui/src/ui/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl NetworkRowController {
status_c.set_text(&format!("Connecting to {}...", ssid_c));
window_c.set_sensitive(false);
let creds = WifiSecurity::WpaPsk { psk: "".into() };
match nm_c.connect(&ssid_c, creds).await {
match nm_c.connect(&ssid_c, None, creds).await {
Ok(_) => {
status_c.set_text("");
on_success_c();
Expand All @@ -153,7 +153,7 @@ impl NetworkRowController {
status_c.set_text(&format!("Connecting to {}...", ssid_c));
window_c.set_sensitive(false);
let creds = WifiSecurity::Open;
match nm_c.connect(&ssid_c, creds).await {
match nm_c.connect(&ssid_c, None, creds).await {
Ok(_) => {
status_c.set_text("");
on_success_c();
Expand Down
10 changes: 5 additions & 5 deletions nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ All notable changes to the `nmrs` crate will be documented in this file.
### Added
- `nmrs::agent` module: NetworkManager secret agent for credential prompting over D-Bus (`SecretAgent`, `SecretAgentBuilder`, `SecretAgentHandle`, `SecretRequest`, `SecretResponder`, `SecretSetting`, `SecretAgentFlags`, `SecretAgentCapabilities`, `CancelReason`, `SecretStoreEvent`)
- `AccessPoint` model preserving per-AP BSSID, frequency, security flags, and device state; `list_access_points(interface)` for full AP enumeration
- `SecurityFeatures`, `ApMode`, `ConnectType` types for decoded NM security capabilities
- `connect_to_bssid(ssid, bssid, creds)` for BSSID-targeted connections
- `Network` gains `best_bssid`, `bssids`, `is_active`, `known`, and `security_features` fields
- `ApBssidNotFound` and `InvalidBssid` error variants
- 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
- 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

### Changed
- Deprecated `wifi_enabled()`, `set_wifi_enabled()`, and `wifi_hardware_enabled()` in favor of `wifi_state()` and `set_wireless_enabled()`
- **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()`.
- `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.5.0"
version = "3.0.0"
authors = ["Akrm Al-Hakimi <alhakimiakrmj@gmail.com>"]
edition.workspace = true
rust-version = "1.94.0"
Expand Down Expand Up @@ -55,3 +55,7 @@ path = "examples/airplane_mode.rs"
[[example]]
name = "ap_list"
path = "examples/ap_list.rs"

[[example]]
name = "multi_wifi"
path = "examples/multi_wifi.rs"
20 changes: 13 additions & 7 deletions nmrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Rust bindings for NetworkManager via D-Bus.
- **VPN Support**: WireGuard VPN connections with full configuration
- **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
- **Real-Time Monitoring**: Signal-based network and device state change notifications
- **Secret Agent**: Respond to NetworkManager credential prompts via an async stream API
Expand All @@ -27,7 +28,7 @@ Rust bindings for NetworkManager via D-Bus.

```toml
[dependencies]
nmrs = "2.0.0"
nmrs = "3.0.0"
```
or
```bash
Expand All @@ -47,16 +48,21 @@ use nmrs::{NetworkManager, WifiSecurity};
async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;

// List networks
let networks = nm.list_networks().await?;
// List networks (None = all Wi-Fi devices, or pass Some("wlan1") to scope)
let networks = nm.list_networks(None).await?;
for net in &networks {
println!("{} - Signal: {}%", net.ssid, net.strength.unwrap_or(0));
}
// Connect to WPA-PSK network
nm.connect("MyNetwork", WifiSecurity::WpaPsk {

// Connect to a WPA-PSK network on the first Wi-Fi device
nm.connect("MyNetwork", None, WifiSecurity::WpaPsk {
psk: "password".into()
}).await?;

// Or scope every operation to a specific radio
let wlan1 = nm.wifi("wlan1");
wlan1.scan().await?;
wlan1.connect("Guest", WifiSecurity::Open).await?;

// Check current connection
if let Some(ssid) = nm.current_ssid().await {
Expand Down Expand Up @@ -117,7 +123,7 @@ use nmrs::{NetworkManager, WifiSecurity, EapOptions, EapMethod, Phase2};
async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;

nm.connect("CorpNetwork", WifiSecurity::WpaEap {
nm.connect("CorpNetwork", None, WifiSecurity::WpaEap {
opts: EapOptions {
identity: "user@company.com".into(),
password: "password".into(),
Expand Down
1 change: 1 addition & 0 deletions nmrs/examples/custom_timeouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async fn main() -> nmrs::Result<()> {
println!("\nConnecting to network...");
nm.connect(
"MyNetwork",
None,
WifiSecurity::WpaPsk {
psk: std::env::var("WIFI_PASSWORD").unwrap_or_else(|_| "password".to_string()),
},
Expand Down
59 changes: 59 additions & 0 deletions nmrs/examples/multi_wifi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! Per-Wi-Fi-device enumeration and scoped operations.
//!
//! Lists every Wi-Fi interface NetworkManager manages, then triggers a
//! scan and prints the visible SSIDs on each radio independently.
//! Useful on laptops with USB Wi-Fi dongles or docks with a second adapter.
//!
//! Run with: `cargo run --example multi_wifi`

use nmrs::NetworkManager;

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

let radios = nm.list_wifi_devices().await?;
if radios.is_empty() {
println!("No Wi-Fi devices found.");
return Ok(());
}

println!("Found {} Wi-Fi radio(s):", radios.len());
for r in &radios {
println!(
" {:<10} {} state={:?} active={}{}",
r.interface,
r.hw_address,
r.state,
r.is_active,
r.active_ssid
.as_ref()
.map(|s| format!(" ssid={s}"))
.unwrap_or_default(),
);
}

for r in &radios {
let scope = nm.wifi(&r.interface);
println!("\n[{}] scanning...", r.interface);

if let Err(e) = scope.scan().await {
eprintln!("[{}] scan failed: {e}", r.interface);
continue;
}
// Give NM a moment to populate scan results.
tokio::time::sleep(std::time::Duration::from_secs(2)).await;

let nets = scope.list_networks().await?;
for n in nets {
println!(
" {:>3}% {:<32} ({} BSSIDs)",
n.strength.unwrap_or(0),
n.ssid,
n.bssids.len(),
);
}
}

Ok(())
}
2 changes: 1 addition & 1 deletion nmrs/examples/wifi_enterprise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async fn main() -> nmrs::Result<()> {
let security = WifiSecurity::WpaEap { opts: eap_opts };

println!("Connecting to enterprise WiFi network...");
nm.connect("CorpNetwork", security).await?;
nm.connect("CorpNetwork", None, security).await?;

println!("Successfully connected to enterprise WiFi!");

Expand Down
4 changes: 2 additions & 2 deletions nmrs/examples/wifi_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;

println!("Scanning for WiFi networks...");
nm.scan_networks().await?;
nm.scan_networks(None).await?;

let networks = nm.list_networks().await?;
let networks = nm.list_networks(None).await?;
for net in networks {
println!("{:30} {}%", net.ssid, net.strength.unwrap_or(0));
}
Expand Down
1 change: 1 addition & 0 deletions nmrs/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
pub mod builders;
pub mod models;
pub mod network_manager;
pub mod wifi_scope;
35 changes: 35 additions & 0 deletions nmrs/src/api/models/device.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::fmt::{Display, Formatter};

use zvariant::OwnedObjectPath;

/// Represents a network device managed by NetworkManager.
///
/// A device can be a WiFi adapter, Ethernet interface, or other network hardware.
Expand Down Expand Up @@ -59,6 +61,39 @@ pub struct Device {
// pub speed: Option<u32>,
}

/// A Wi-Fi device summary returned by
/// [`list_wifi_devices`](crate::NetworkManager::list_wifi_devices).
///
/// Use this on multi-radio machines (laptops with USB dongles, docks with a
/// second wireless adapter, etc.) to discover the available interfaces and
/// pick one to scope subsequent operations to. Pair with
/// [`NetworkManager::wifi`](crate::NetworkManager::wifi) for ergonomic
/// per-interface calls.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct WifiDevice {
/// D-Bus object path of the device.
pub path: OwnedObjectPath,
/// Interface name (e.g. `"wlan0"`).
pub interface: String,
/// Current MAC address (may be randomized).
pub hw_address: String,
/// Permanent (factory-burned) MAC, if NM exposes it.
pub permanent_hw_address: Option<String>,
/// Kernel driver name, if available.
pub driver: Option<String>,
/// Current device state.
pub state: DeviceState,
/// Whether NetworkManager manages this device.
pub managed: bool,
/// Whether NM will autoconnect known networks on this device.
pub autoconnect: bool,
/// `true` if the device currently has an active access point.
pub is_active: bool,
/// SSID of the currently active AP, if any.
pub active_ssid: Option<String>,
}

/// Represents the hardware identity of a network device.
///
/// Contains MAC addresses that uniquely identify the device. The permanent
Expand Down
18 changes: 16 additions & 2 deletions nmrs/src/api/models/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use super::state_reason::StateReason;
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// match nm.connect("MyNetwork", WifiSecurity::WpaPsk {
/// match nm.connect("MyNetwork", None, WifiSecurity::WpaPsk {
/// psk: "password".into()
/// }).await {
/// Ok(_) => println!("Connected!"),
Expand Down Expand Up @@ -48,7 +48,7 @@ use super::state_reason::StateReason;
/// let nm = NetworkManager::new().await?;
///
/// for attempt in 1..=3 {
/// match nm.connect("MyNetwork", WifiSecurity::Open).await {
/// match nm.connect("MyNetwork", None, WifiSecurity::Open).await {
/// Ok(_) => {
/// println!("Connected on attempt {}", attempt);
/// break;
Expand Down Expand Up @@ -202,6 +202,20 @@ pub enum ConnectionError {
#[error("invalid BSSID format: '{0}' (expected XX:XX:XX:XX:XX:XX)")]
InvalidBssid(String),

/// Interface exists but is not a Wi-Fi device.
#[error("interface '{interface}' is not a Wi-Fi device")]
NotAWifiDevice {
/// The interface name that was checked.
interface: String,
},

/// No Wi-Fi device with the given interface name.
#[error("no Wi-Fi device named '{interface}'")]
WifiInterfaceNotFound {
/// The interface name that was searched for.
interface: String,
},

/// A radio is hardware-disabled via rfkill.
#[error("radio is hardware-disabled (rfkill)")]
HardwareRadioKilled,
Expand Down
12 changes: 6 additions & 6 deletions nmrs/src/api/models/wifi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ use super::access_point::SecurityFeatures;
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// // Scan for networks
/// nm.scan_networks().await?;
/// let networks = nm.list_networks().await?;
/// // Scan for networks (None = all Wi-Fi devices)
/// nm.scan_networks(None).await?;
/// let networks = nm.list_networks(None).await?;
///
/// for net in networks {
/// println!("SSID: {}", net.ssid);
Expand Down Expand Up @@ -86,7 +86,7 @@ pub struct Network {
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
/// let networks = nm.list_networks().await?;
/// let networks = nm.list_networks(None).await?;
///
/// if let Some(network) = networks.first() {
/// let info = nm.show_details(network).await?;
Expand Down Expand Up @@ -564,7 +564,7 @@ impl EapOptionsBuilder {
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// nm.connect("HomeWiFi", WifiSecurity::WpaPsk {
/// nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk {
/// psk: "my_secure_password".into()
/// }).await?;
/// # Ok(())
Expand All @@ -585,7 +585,7 @@ impl EapOptionsBuilder {
/// .with_method(EapMethod::Peap)
/// .with_phase2(Phase2::Mschapv2);
///
/// nm.connect("CorpWiFi", WifiSecurity::WpaEap {
/// nm.connect("CorpWiFi", None, WifiSecurity::WpaEap {
/// opts: eap_opts
/// }).await?;
/// # Ok(())
Expand Down
Loading