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
1 change: 1 addition & 0 deletions docs/src/api/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct Device {
pub device_type: DeviceType,
pub state: DeviceState,
pub managed: Option<bool>,
pub autoconnect: Option<bool>,
pub driver: Option<String>,
pub ip4_address: Option<String>,
pub ip6_address: Option<String>,
Expand Down
2 changes: 2 additions & 0 deletions docs/src/api/network-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ devices that NetworkManager reports as `veth`.
| `list_wired_devices()` | `Result<Vec<Device>>` | List Ethernet devices |
| `list_wired_device_details()` | `Result<Vec<WiredDevice>>` | List Ethernet devices with link speed, active connection id, and IPs |
| `get_device_by_interface(name)` | `Result<OwnedObjectPath>` | Find device by interface name |
| `set_device_autoconnect(name, bool)` | `Result<()>` | Allow or prevent automatic connection activation on one device |
| `set_device_managed(name, bool)` | `Result<()>` | Temporarily make NetworkManager manage or ignore one device |
| `is_connecting()` | `Result<bool>` | Check if any device is connecting |
| `list_active_connections()` | `Result<Vec<ActiveConnection>>` | List typed active wired, Wi-Fi, VPN, and other connections |
| `snapshot()` | `Result<NetworkSnapshot>` | Read point-in-time applet state after a `NetworkEvent` |
Expand Down
6 changes: 6 additions & 0 deletions nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to the `nmrs` crate will be documented in this file.

## [Unreleased]

### Added

- Device snapshots now expose per-device autoconnect state, and
`NetworkManager::set_device_autoconnect()` / `set_device_managed()` provide
high-level control of both writable properties. ([#541](https://github.com/freedesktop-rs/nmrs/issues/541))

## [3.5.2] - 2026-09-05
### Fixed
- `#[deprecated(since = ...)]` on `connect_vpn_by_uuid()` and `disconnect_vpn_by_uuid()` said `3.6.0`; corrected to `3.5.1`, the release that actually deprecated them.([#544](https://github.com/freedesktop-rs/nmrs/pull/544))
Expand Down
2 changes: 2 additions & 0 deletions nmrs/src/api/models/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub struct Device {
pub state: DeviceState,
/// Whether NetworkManager manages this device
pub managed: Option<bool>,
/// Whether NetworkManager may automatically activate a connection on this device.
pub autoconnect: Option<bool>,
/// Kernel driver name
pub driver: Option<String>,
/// Assigned IPv4 address with CIDR notation (only present when connected)
Expand Down
1 change: 1 addition & 0 deletions nmrs/src/api/models/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ fn device_with_type(device_type: DeviceType) -> Device {
device_type,
state: DeviceState::Activated,
managed: Some(true),
autoconnect: Some(true),
driver: Some("test".into()),
ip4_address: None,
ip6_address: None,
Expand Down
59 changes: 58 additions & 1 deletion nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::core::custom_connection::{
};
use crate::core::device::{
is_connecting, list_bluetooth_devices, list_devices, list_wired_device_details,
wait_for_wifi_ready,
set_device_autoconnect, set_device_managed, wait_for_wifi_ready,
};
use crate::core::saved_connection as saved_profiles;
use crate::core::scan::{current_network, list_access_points, list_networks, scan_networks};
Expand Down Expand Up @@ -368,6 +368,63 @@ impl NetworkManager {
list_devices(&self.conn).await
}

/// Allows or prevents automatic connection activation on one device.
///
/// This changes NetworkManager's per-device `Autoconnect` property. Setting
/// it to `false` does not disconnect a connection that is already active;
/// it only prevents future automatic activation until the property is set
/// to `true` or a connection is activated manually.
///
/// # Errors
///
/// Returns [`InterfaceNotFound`](crate::ConnectionError::InterfaceNotFound)
/// if no device has the supplied interface name. D-Bus and authorization
/// failures are returned as [`ConnectionError`](crate::ConnectionError).
///
/// # Example
///
/// ```no_run
/// use nmrs::NetworkManager;
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
/// nm.set_device_autoconnect("eth0", false).await?;
/// # Ok(())
/// # }
/// ```
pub async fn set_device_autoconnect(&self, interface: &str, autoconnect: bool) -> Result<()> {
set_device_autoconnect(&self.conn, interface, autoconnect).await
}

/// Sets whether NetworkManager manages one device.
///
/// Setting this to `false` makes NetworkManager stop managing the device
/// and may tear down its active connection. The setting is temporary and
/// is lost when NetworkManager restarts. This uses the writable `Managed`
/// property so it also works with NetworkManager releases older than 1.58;
/// the newer `SetManaged` D-Bus method is required only for persistence.
///
/// # Errors
///
/// Returns [`InterfaceNotFound`](crate::ConnectionError::InterfaceNotFound)
/// if no device has the supplied interface name. D-Bus and authorization
/// failures are returned as [`ConnectionError`](crate::ConnectionError).
///
/// # Example
///
/// ```no_run
/// use nmrs::NetworkManager;
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
/// nm.set_device_managed("eth0", false).await?;
/// # Ok(())
/// # }
/// ```
pub async fn set_device_managed(&self, interface: &str, managed: bool) -> Result<()> {
set_device_managed(&self.conn, interface, managed).await
}

/// List all bluetooth devices.
pub async fn list_bluetooth_devices(&self) -> Result<Vec<BluetoothDevice>> {
list_bluetooth_devices(&self.conn).await
Expand Down
57 changes: 57 additions & 0 deletions nmrs/src/core/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
None
}
};
let autoconnect = match d_proxy.autoconnect().await {
Ok(value) => Some(value),
Err(e) => {
trace!(
"Failed to get 'autoconnect' property for device {}: {}",
interface, e
);
None
}
};
let driver = match d_proxy.driver().await {
Ok(d) => Some(d),
Err(e) => {
Expand Down Expand Up @@ -165,6 +175,7 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
device_type,
state,
managed,
autoconnect,
driver,
ip4_address,
ip6_address,
Expand All @@ -175,6 +186,52 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
Ok(devices)
}

/// Sets whether NetworkManager may automatically activate connections on a device.
pub(crate) async fn set_device_autoconnect(
conn: &Connection,
interface: &str,
autoconnect: bool,
) -> Result<()> {
let path = get_device_by_interface(conn, interface)
.await
.map_err(|error| match error {
ConnectionError::NotFound => ConnectionError::InterfaceNotFound(interface.to_string()),
other => other,
})?;
let device = NMDeviceProxy::builder(conn).path(path)?.build().await?;

device
.set_autoconnect(autoconnect)
.await
.map_err(|source| ConnectionError::DbusOperation {
context: format!("failed to set Autoconnect on {interface}"),
source,
})
}

/// Sets whether NetworkManager manages a device.
pub(crate) async fn set_device_managed(
conn: &Connection,
interface: &str,
managed: bool,
) -> Result<()> {
let path = get_device_by_interface(conn, interface)
.await
.map_err(|error| match error {
ConnectionError::NotFound => ConnectionError::InterfaceNotFound(interface.to_string()),
other => other,
})?;
let device = NMDeviceProxy::builder(conn).path(path)?.build().await?;

device
.set_managed(managed)
.await
.map_err(|source| ConnectionError::DbusOperation {
context: format!("failed to set Managed on {interface}"),
source,
})
}

/// Lists wired Ethernet devices with Ethernet-specific details.
pub(crate) async fn list_wired_device_details(conn: &Connection) -> Result<Vec<WiredDevice>> {
let proxy = NMProxy::new(conn).await?;
Expand Down
4 changes: 4 additions & 0 deletions nmrs/src/dbus/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ pub trait NMDevice {
#[zbus(property)]
fn managed(&self) -> Result<bool>;

/// Set whether NetworkManager manages this device until the daemon restarts.
#[zbus(property)]
fn set_managed(&self, value: bool) -> Result<()>;

/// The kernel driver in use.
#[zbus(property)]
fn driver(&self) -> Result<String>;
Expand Down
96 changes: 95 additions & 1 deletion nmrs/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ async fn disconnect_device(nm: &NetworkManager, interface: &str) -> nmrs::Result

async fn cleanup_wired_profile(nm: &NetworkManager, interface: &str) -> Vec<String> {
let mut failures = Vec::new();
match timeout(DBUS_TIMEOUT, nm.set_device_managed(interface, true)).await {
Ok(Ok(())) => {}
Ok(Err(error)) => failures.push(format!("restore managed state for {interface}: {error}")),
Err(_) => failures.push(format!("restore managed state for {interface}: timed out")),
}
match timeout(DBUS_TIMEOUT, nm.set_device_autoconnect(interface, true)).await {
Ok(Ok(())) => {}
Ok(Err(error)) => failures.push(format!("restore autoconnect for {interface}: {error}")),
Err(_) => failures.push(format!("restore autoconnect for {interface}: timed out")),
}
match timeout(DBUS_TIMEOUT, disconnect_device(nm, interface)).await {
Ok(Ok(())) => {}
Ok(Err(error)) => failures.push(format!("disconnect {interface}: {error}")),
Expand Down Expand Up @@ -967,10 +977,29 @@ async fn wired_connection_lifecycle() {
.find(|device| device.interface == interface)
.unwrap_or_else(|| {
panic!("managed veth interface {interface:?} was missing: {devices:?}")
});
});
assert_eq!(device.managed, Some(true));
assert_eq!(device.autoconnect, Some(true));
assert!(!device.path.is_empty());

let missing_interface = "nmrs-missing-interface";
let error = bounded(
"reject autoconnect changes for a missing interface",
DBUS_TIMEOUT,
nm.set_device_autoconnect(missing_interface, false),
)
.await
.expect_err("missing interface unexpectedly accepted an autoconnect change");
assert!(matches!(error, ConnectionError::InterfaceNotFound(name) if name == missing_interface));
let error = bounded(
"reject managed-state changes for a missing interface",
DBUS_TIMEOUT,
nm.set_device_managed(missing_interface, false),
)
.await
.expect_err("missing interface unexpectedly accepted a managed-state change");
assert!(matches!(error, ConnectionError::InterfaceNotFound(name) if name == missing_interface));

let details = bounded(
"list detailed wired devices",
DBUS_TIMEOUT,
Expand Down Expand Up @@ -1050,6 +1079,71 @@ async fn wired_connection_lifecycle() {
.is_some_and(|address| address.starts_with("192.168.251."))
);

bounded(
"disable autoconnect on the active veth client",
DBUS_TIMEOUT,
nm.set_device_autoconnect(&interface, false),
)
.await
.expect("failed to disable device autoconnect");
let devices = bounded("refresh devices", DBUS_TIMEOUT, nm.list_wired_devices())
.await
.expect("failed to refresh wired devices");
let device = devices
.iter()
.find(|device| device.interface == interface)
.expect("veth disappeared after disabling autoconnect");
assert_eq!(device.autoconnect, Some(false));
assert!(active_connections(&nm).await.iter().any(
|connection| matches!(connection, ActiveConnection::Wired(wired) if wired.uuid == saved_uuid)
));

bounded(
"make the active veth client unmanaged",
DBUS_TIMEOUT,
nm.set_device_managed(&interface, false),
)
.await
.expect("failed to make device unmanaged");
timeout(EVENT_TIMEOUT, async {
loop {
let devices = bounded("refresh unmanaged device", DBUS_TIMEOUT, nm.list_devices())
.await
.expect("failed to refresh devices after changing managed state");
let device = devices
.iter()
.find(|device| device.interface == interface)
.expect("veth disappeared after changing managed state");
let connection_is_active = active_connections(&nm).await.iter().any(
|connection| matches!(connection, ActiveConnection::Wired(wired) if wired.uuid == saved_uuid),
);
if device.managed == Some(false)
&& device.state == DeviceState::Unmanaged
&& !connection_is_active
{
break;
}
sleep(Duration::from_millis(25)).await;
}
})
.await
.expect("managed=false did not deactivate the veth connection");

bounded(
"restore the managed veth client",
DBUS_TIMEOUT,
nm.set_device_managed(&interface, true),
)
.await
.expect("failed to restore managed state");
bounded(
"restore autoconnect on the managed veth client",
DBUS_TIMEOUT,
nm.set_device_autoconnect(&interface, true),
)
.await
.expect("failed to restore device autoconnect");

bounded(
"disconnect the managed veth client",
DBUS_TIMEOUT,
Expand Down