feat(networking): replace custom NetworkManager backend with nmrs

Switch the networking pages to use nmrs 3.4.0 for NetworkManager
state, device lists, Wi-Fi operations, VPN imports/activation, and
secret-agent handling.

Remove the old cosmic-settings-network-manager-subscription crate,
drop the direct dbus-settings NetworkManager dependencies, and delete
the unused nmcli VPN helper. Keep nm-connection-editor for profile
creation/editing where the UI still depends on it.

Carry Wi-Fi security metadata through the password flow so SAE/WPA3
personal networks can be activated with SAE key management, and make
failed/cancelled auth attempts clear pending connection state
This commit is contained in:
Akrm Al-Hakimi 2026-07-08 18:34:35 -04:00 • committed by Ashley Wulber
parent fff1485876
commit 04d5f46d0d
18 changed files with 2134 additions and 3504 deletions

View file

@ -21,8 +21,6 @@ color-eyre = "0.6.5"
cosmic-bg-config.workspace = true
cosmic-comp-config = { workspace = true, optional = true }
cosmic-config.workspace = true
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
cosmic-idle-config.workspace = true
cosmic-panel-config = { workspace = true, optional = true }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true }
@ -34,7 +32,6 @@ cosmic-settings-accessibility-subscription = { path = "../subscriptions/accessib
cosmic-settings-a11y-manager-subscription = { path = "../subscriptions/a11y-manager", optional = true }
cosmic-settings-airplane-mode-subscription = { path = "../subscriptions/airplane-mode", optional = true }
cosmic-settings-bluetooth-subscription = { path = "../subscriptions/bluetooth", optional = true }
cosmic-settings-network-manager-subscription = { path = "../subscriptions/network-manager", optional = true }
cosmic-settings-sound = { path = "../pages/sound", optional = true }
cosmic-settings-upower-subscription = { path = "../subscriptions/upower", optional = true }
cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
@ -57,11 +54,13 @@ locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional =
sysinfo = { version = "=0.38.0", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true }
notify = "8.2.0"
nmrs = { version = "3.4.0", optional = true }
regex = "1.12.3"
ron = "0.12"
rust-embed = "8.11.0"
sctk = { workspace = true, optional = true }
secure-string = "0.3.0"
secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"], optional = true }
serde = { version = "1.0.228", features = ["derive"] }
slab = "0.4.12"
slotmap = "1.1.1"
@ -169,10 +168,9 @@ page-input = [
]
page-legacy-applications = ["cosmic-comp-config", "dep:cosmic-randr"]
page-networking = [
"dep:cosmic-settings-network-manager-subscription",
"xdg-portal",
"dep:cosmic-dbus-networkmanager",
"dep:nm-secret-agent-manager",
"dep:nmrs",
"dep:secret-service",
"dep:zbus",
]
page-power = ["dep:upower_dbus", "dep:zbus"]

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,24 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod backend;
pub mod vpn;
pub mod wifi;
pub mod wired;
use std::ffi::OsStr;
use std::process::Stdio;
use std::sync::Arc;
use anyhow::Context;
use cosmic::{Apply, Element, Task, widget};
use cosmic_dbus_networkmanager::interface::enums::{DeviceState, DeviceType};
use cosmic_dbus_networkmanager::nm::NetworkManager;
use cosmic_settings_network_manager_subscription as network_manager;
use cosmic_settings_page::{self as page, Section, section};
use futures::{SinkExt, StreamExt};
use secure_string::SecureString;
use slotmap::SlotMap;
use self::backend as network_manager;
use self::backend::devices::{DeviceState, DeviceType};
pub type SecretSender = Arc<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<SecureString>>>>;
static NM_CONNECTION_EDITOR: &str = "nm-connection-editor";
@ -37,8 +37,8 @@ pub struct Page {
pub enum Message {
/// An error occurred.
Error(String),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Open the wifi settings page with the selected device.
OpenPage {
page: page::Entity,
@ -143,6 +143,7 @@ impl page::Page<crate::pages::Message> for Page {
fl!("network-device-state", "unavailable")
}
DeviceState::Unknown => fl!("network-device-state", "unknown"),
DeviceState::Other(_) => fl!("network-device-state", "unknown"),
DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"),
},
"preferences-wireless-symbolic",
@ -190,6 +191,7 @@ impl page::Page<crate::pages::Message> for Page {
fl!("network-device-state", "unplugged")
}
DeviceState::Unknown => fl!("network-device-state", "unknown"),
DeviceState::Other(_) => fl!("network-device-state", "unknown"),
DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"),
},
"preferences-wired-symbolic",
@ -225,9 +227,9 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
if self.nm_task.is_none() {
return cosmic::Task::future(async move {
zbus::Connection::system()
nmrs::NetworkManager::new()
.await
.context("failed to create system dbus connection")
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
@ -310,33 +312,47 @@ impl Page {
Task::none()
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, nm: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(why) => {
tracing::error!(
why = why.to_string(),
"failed to connect to network_manager"
);
return futures::future::pending().await;
match network_manager::devices::list(&nm, |_| true).await {
Ok(devices) => {
_ = sender
.send(crate::pages::Message::Networking(Message::UpdateDevices(
devices.into_iter().map(Arc::new).collect(),
)))
.await;
}
};
Err(why) => {
_ = sender
.send(crate::pages::Message::Networking(Message::Error(
why.to_string(),
)))
.await;
}
}
let mut devices_changed =
std::pin::pin!(network_manager.receive_devices_changed().await.then(
|_| async {
match network_manager::devices::list(&conn, |_| true).await {
Ok(devices) => Message::UpdateDevices(
let Ok(events) = nm.network_events().await else {
return futures::future::pending().await;
};
let devices_changed = events.filter_map(|event| async {
match event {
Ok(nmrs::NetworkEvent::DeviceChanged { .. })
| Ok(nmrs::NetworkEvent::SettingsChanged(_))
| Ok(nmrs::NetworkEvent::NetworkManagerRestarted) => {
match network_manager::devices::list(&nm, |_| true).await {
Ok(devices) => Some(Message::UpdateDevices(
devices.into_iter().map(Arc::new).collect(),
),
Err(why) => Message::Error(why.to_string()),
)),
Err(why) => Some(Message::Error(why.to_string())),
}
}
));
Ok(_) => None,
Err(why) => Some(Message::Error(why.to_string())),
}
});
futures::pin_mut!(devices_changed);
while let Some(message) = devices_changed.next().await {
_ = sender
@ -353,16 +369,6 @@ impl Page {
}
}
async fn nm_add_vpn_file<P: AsRef<OsStr>>(type_: &str, path: P) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args(["connection", "import", "type", type_, "file"])
.arg(path)
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}
async fn nm_add_wired() -> Result<(), String> {
nm_connection_editor(&["--type=802-3-ethernet", "-c"]).await
}

View file

@ -1,8 +1,6 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod nmcli;
use std::collections::HashMap;
use std::sync::{Arc, LazyLock};
@ -14,17 +12,16 @@ use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::widget::text_input::focus;
use cosmic::widget::{self, icon};
use cosmic::{Apply, Element, Task, task};
use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo;
use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag};
use cosmic_settings_network_manager_subscription::{
self as network_manager, NetworkManagerState, UUID,
};
use cosmic_settings_page::{self as page, Section, section};
use futures::{FutureExt, SinkExt, StreamExt};
use indexmap::IndexMap;
use secure_string::SecureString;
use tokio::sync::Mutex;
use super::backend as network_manager;
use super::backend::current_networks::ActiveConnectionInfo;
use super::backend::nm_secret_agent::{self, PasswordFlag};
use super::backend::{NetworkManagerState, UUID};
use crate::pages::networking::SecretSender;
pub static SECURE_INPUT_VPN: LazyLock<widget::Id> = LazyLock::new(widget::Id::unique);
@ -60,8 +57,8 @@ pub enum Message {
NetworkManager(network_manager::Event),
/// An update from the secret agent
SecretAgent(network_manager::nm_secret_agent::Event),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Updates the password text input
PasswordUpdate(SecureString),
/// Refresh devices and their connection profiles
@ -181,7 +178,7 @@ pub enum VpnDialog {
#[derive(Debug)]
pub struct NmState {
conn: zbus::Connection,
conn: nmrs::NetworkManager,
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
active_conns: Vec<ActiveConnectionInfo>,
devices: Vec<network_manager::devices::DeviceInfo>,
@ -371,20 +368,22 @@ impl page::Page<crate::pages::Message> for Page {
let (tx, rx) = tokio::sync::mpsc::channel(4);
self.secret_tx = Some(tx);
if self.nm_task.is_none() {
return cosmic::Task::batch([cosmic::task::future(async move {
zbus::Connection::system()
.await
.context("failed to create system dbus connection")
.map_or_else(
|why| Message::Error(ErrorKind::DbusConnection, why.to_string()),
Message::NetworkManagerConnect,
)
}),
cosmic::Task::stream(
cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", rx),
)
.map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))),
]);
return cosmic::Task::batch([
cosmic::task::future(async move {
nmrs::NetworkManager::new()
.await
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(ErrorKind::DbusConnection, why.to_string()),
Message::NetworkManagerConnect,
)
}),
cosmic::Task::stream(nm_secret_agent::secret_agent_stream(
"com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent",
rx,
))
.map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))),
]);
}
cosmic::Task::none()
@ -495,9 +494,8 @@ impl Page {
Message::WireGuardConfig => {
if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() {
return cosmic::task::future(async move {
let new_path = path.replace(&filename, &device);
_ = std::fs::rename(&path, &new_path);
match super::nm_add_vpn_file("wireguard", new_path).await {
let _ = filename;
match network_manager::import_wireguard(path, &device).await {
Ok(_) => Message::Refresh,
Err(why) => Message::Error(ErrorKind::Config, why.to_string()),
}
@ -510,18 +508,12 @@ impl Page {
if let Some(settings) = self.known_connections.get(&uuid) {
let settings = match settings {
ConnectionSettings::Vpn(settings) => settings,
ConnectionSettings::Wireguard { id } => {
let connection_name = id.clone();
return cosmic::task::future(async move {
if let Err(why) = nmcli::connect(&connection_name).await {
return Message::Error(
ErrorKind::Connect,
format!("failed to connect to WireGuard VPN: {why}"),
);
}
Message::Refresh
});
ConnectionSettings::Wireguard { .. } => {
if let Some(NmState { ref sender, .. }) = self.nm_state {
_ = sender
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
}
return Task::none();
}
};
@ -541,28 +533,10 @@ impl Page {
return task::message(Message::FocusSecureInput);
}
_ => {
let connection_name = settings.id.clone();
let username = settings.username.clone();
return cosmic::task::future(async move {
if let Err(why) = nmcli::connect(&connection_name).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((
ErrorKind::Connect,
format!("failed to connect to VPN: {why}"),
)),
id: connection_name.clone(),
uuid,
username: username.clone(),
description: None,
password: SecureString::from(""),
password_hidden: true,
// TODO grab from the current dialog
tx: Arc::new(Mutex::new(None)),
});
}
Message::Refresh
});
if let Some(NmState { ref sender, .. }) = self.nm_state {
_ = sender
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
}
}
}
}
@ -629,89 +603,55 @@ impl Page {
};
if let VpnDialog::Password {
id,
id: _,
uuid,
username,
password,
tx,
..
} = dialog
&& let Some(NmState { ref sender, .. }) = self.nm_state
{
let username_unwrapped = username.clone().unwrap_or_default();
let task = self.activate_with_password(
id.clone(),
uuid.clone(),
username_unwrapped.clone(),
password.clone(),
);
let sec_tx = self.secret_tx.clone();
return task
.then(move |_| {
let sec_tx = sec_tx.clone();
let uuid = uuid.clone();
let username = username.clone();
let password = password.clone();
let tx = tx.clone();
let id = id.clone();
Task::future(async move {
let mut guard = tx.lock().await;
if let Some(sender) = guard.take() {
let _ = sender.send(password);
} else {
// apply password and username then
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) =
tokio::sync::oneshot::channel();
if let Err(err) = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([
// username and password
(
"username".to_string(),
username.clone().unwrap_or_default().into(),
),
("password".to_string(), password.clone()),
]),
applied_tx,
})
.await
{
tracing::error!(%err, "failed to apply secret");
}
// wait max 1s for the applied signal
if let Err(err) = tokio::time::timeout(
std::time::Duration::from_secs(1),
applied_rx,
)
.await
{
tracing::error!(%err, "failed to apply secret");
}
}
// activate
if let Err(why) = nmcli::connect(&id).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((
ErrorKind::Connect,
format!("failed to connect to VPN: {why}"),
)),
id: id.clone(),
uuid,
username: username.clone(),
description: None,
password,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
}
let nm_sender = sender.clone();
return Task::future(async move {
let mut guard = tx.lock().await;
if let Some(sender) = guard.take() {
let _ = sender.send(password);
} else {
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
if let Err(err) = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([
("username".to_string(), username_unwrapped.into()),
("password".to_string(), password),
]),
applied_tx,
})
.await
{
tracing::error!(%err, "failed to apply secret");
}
if let Err(err) = tokio::time::timeout(
std::time::Duration::from_secs(1),
applied_rx,
)
.await
{
tracing::error!(%err, "failed to apply secret");
}
}
_ = nm_sender
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
}
Message::Refresh
})
})
.map(crate::app::Message::from);
Message::Refresh
})
.map(crate::app::Message::from);
}
}
Message::RetryWithPassword => {
@ -720,53 +660,39 @@ impl Page {
};
if let VpnDialog::Password {
id,
id: _,
uuid,
username,
password,
..
} = dialog
&& let Some(NmState { ref sender, .. }) = self.nm_state
{
let username_unwrapped = username.unwrap_or_default();
let sec_tx = self.secret_tx.clone();
let task = self.activate_with_password(
id.clone(),
uuid.clone(),
username_unwrapped.clone(),
password.clone(),
);
return task
.then(move |_| {
let sec_tx = sec_tx.clone();
let uuid = uuid.clone();
let username = username_unwrapped.clone();
let password = password.clone();
Task::future(async move {
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
let _ = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([
// username and password
("username".to_string(), username.clone().into()),
("password".to_string(), password.clone()),
]),
applied_tx,
})
.await;
// wait max 1s for the applied signal
let _ = tokio::time::timeout(
std::time::Duration::from_secs(1),
applied_rx,
)
let nm_sender = sender.clone();
return Task::future(async move {
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
let _ = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([
("username".to_string(), username_unwrapped.into()),
("password".to_string(), password),
]),
applied_tx,
})
.await;
let _ =
tokio::time::timeout(std::time::Duration::from_secs(1), applied_rx)
.await;
}
Message::Activate(uuid)
})
})
.map(crate::app::Message::from);
}
_ = nm_sender.unbounded_send(network_manager::Request::ActivateVpn(uuid));
Message::Refresh
})
.map(crate::app::Message::from);
}
}
Message::UsernameUpdate(user) => {
@ -873,56 +799,14 @@ impl Page {
Task::none()
}
fn activate_with_password(
&mut self,
connection_name: String,
uuid: Arc<str>,
username: String,
password: SecureString,
) -> Task<Message> {
cosmic::task::future(async move {
if let Err(why) = nmcli::set_username(&connection_name, &username).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((ErrorKind::WithPassword("username"), why.to_string())),
id: connection_name.clone(),
uuid,
username: Some(username),
description: None,
password,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
}
if let Err(why) = nmcli::add_fallback(&connection_name).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((ErrorKind::Config, why.to_string())),
id: connection_name.clone(),
uuid,
username: Some(username),
password,
description: None,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
}
Message::Refresh
})
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let watchers = std::pin::pin!(async move {
futures::join!(
network_manager::watch(conn.clone(), tx.clone()),
network_manager::active_conns::watch(conn.clone(), tx.clone()),
network_manager::devices::watch(conn, true, tx)
)
network_manager::watch(conn, tx).await;
});
let forwarder = std::pin::pin!(async move {
@ -1141,7 +1025,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
.into()
}
fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
@ -1150,7 +1034,7 @@ fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard);
@ -1208,7 +1092,9 @@ fn add_network() -> Task<crate::app::Message> {
);
};
super::nm_add_vpn_file("openvpn", path).await
network_manager::import_openvpn(path)
.await
.map_err(|why| why.to_string())
};
match result {
@ -1223,108 +1109,96 @@ fn add_network() -> Task<crate::app::Message> {
.apply(cosmic::task::future)
}
fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
let settings = async move {
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
_ = settings.load_connections(&[]).await;
let settings = settings
// Get a list of known connections.
.list_connections()
.await?
// Prepare for wrapping in a concurrent stream.
.into_iter()
.map(|conn| async move { conn })
// Create a concurrent stream for each connection.
.apply(futures::stream::FuturesOrdered::from_iter)
// Concurrently fetch settings for each connection, and filter for VPN.
.filter_map(|conn| async move {
let settings = conn.get_settings().await.ok()?;
let connection = settings.get("connection")?;
match connection
.get("type")?
.downcast_ref::<String>()
.ok()?
.as_str()
{
"vpn" => (),
"wireguard" => {
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
return Some((Arc::from(uuid), ConnectionSettings::Wireguard { id }));
}
_ => return None,
}
let vpn = settings.get("vpn")?;
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
let (connection_type, username, password_flag) = vpn
.get("data")
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok())
.map(|dict| {
let (mut connection_type, mut password_flag) = (None, None);
let mut username = vpn
.get("user-name")
.and_then(|u| u.downcast_ref::<String>().ok());
if dict
.get::<String, String>(&String::from("connection-type"))
.ok()
.flatten()
.as_deref()
// may be "password" or "password-tls"
.is_some_and(|p| p.starts_with("password"))
{
connection_type = Some(ConnectionType::Password);
username = Some(username.unwrap_or_default());
password_flag = dict
.get::<String, String>(&String::from("password-flags"))
.ok()
.flatten()
.and_then(|value| match value.as_str() {
"0" => Some(PasswordFlag::None),
"1" => Some(PasswordFlag::AgentOwned),
"2" => Some(PasswordFlag::NotSaved),
"4" => Some(PasswordFlag::NotRequired),
_ => None,
});
}
(connection_type, username, password_flag)
})
.unwrap_or_default();
Some((
Arc::from(uuid),
ConnectionSettings::Vpn(VpnConnectionSettings {
id,
connection_type,
password_flag,
username,
}),
))
})
// Reduce the settings list into
.fold(IndexMap::new(), |mut set, (uuid, data)| async move {
set.insert(uuid, data);
set
})
.await;
Ok::<_, zbus::Error>(settings)
};
fn connection_settings(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let settings = async move {
let vpns = conn.list_vpn_connections().await?;
let mut settings = IndexMap::new();
for vpn in vpns {
let uuid = Arc::from(vpn.uuid.as_str());
let data = match vpn.vpn_type {
nmrs::VpnType::WireGuard { .. } => ConnectionSettings::Wireguard { id: vpn.id },
nmrs::VpnType::OpenVpn {
connection_type,
user_name,
password_flags,
..
} => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: user_name,
connection_type: connection_type
.filter(|ct| {
matches!(
ct,
nmrs::OpenVpnConnectionType::Password
| nmrs::OpenVpnConnectionType::PasswordTls
)
})
.map(|_| ConnectionType::Password),
password_flag: Some(password_flag(password_flags.0)),
}),
nmrs::VpnType::OpenConnect {
user_name,
password_flags,
..
}
| nmrs::VpnType::StrongSwan {
user_name,
password_flags,
..
}
| nmrs::VpnType::Pptp {
user_name,
password_flags,
..
}
| nmrs::VpnType::L2tp {
user_name,
password_flags,
..
} => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: user_name,
connection_type: Some(ConnectionType::Password),
password_flag: Some(password_flag(password_flags.0)),
}),
nmrs::VpnType::Generic {
user_name,
password_flags,
..
} => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: user_name,
connection_type: Some(ConnectionType::Password),
password_flag: Some(password_flag(password_flags.0)),
}),
_ => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: None,
connection_type: None,
password_flag: None,
}),
};
settings.insert(uuid, data);
}
Ok::<_, nmrs::ConnectionError>(settings)
};
settings.await.map_or_else(
|why| Message::Error(ErrorKind::ConnectionSettings, why.to_string()),
Message::KnownConnections,
)
})
}
fn password_flag(value: u32) -> PasswordFlag {
match value {
0 => PasswordFlag::None,
1 => PasswordFlag::AgentOwned,
2 => PasswordFlag::NotSaved,
4 => PasswordFlag::NotRequired,
_ => PasswordFlag::AgentOwned,
}
}

View file

@ -1,38 +0,0 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::Apply;
use std::process::Stdio;
pub async fn set_username(connection_name: &str, username: &str) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args(["con", "mod", connection_name, "vpn.user-name", username])
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}
pub async fn add_fallback(connection_name: &str) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args([
"con",
"mod",
connection_name,
"+vpn.data",
"data-ciphers=AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305:AES-256-CBC:AES-128-CBC",
])
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}
pub async fn connect(connection_name: &str) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args(["con", "up", connection_name])
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}

View file

@ -13,16 +13,15 @@ use cosmic::widget::space::horizontal;
use cosmic::widget::text_input::focus;
use cosmic::widget::{self, column, icon};
use cosmic::{Apply, Element, Task, task};
use cosmic_settings_network_manager_subscription::available_wifi::{AccessPoint, NetworkType};
use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo;
use cosmic_settings_network_manager_subscription::{
self as network_manager, NetworkManagerState, nm_secret_agent,
};
use cosmic_settings_page::{self as page, Section, section};
use futures::{SinkExt, StreamExt};
use secure_string::SecureString;
use tokio::sync::Mutex;
use super::backend as network_manager;
use super::backend::available_wifi::{AccessPoint, NetworkType};
use super::backend::current_networks::ActiveConnectionInfo;
use super::backend::{NetworkManagerState, nm_secret_agent};
use crate::pages::networking::SecretSender;
pub static SECURE_INPUT_WIFI: LazyLock<widget::Id> = LazyLock::new(widget::Id::unique);
@ -55,8 +54,8 @@ pub enum Message {
NetworkManager(network_manager::Event),
/// An update from the secret agent
SecretAgent(network_manager::nm_secret_agent::Event),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Request an auth dialog
PasswordRequest(network_manager::SSID),
/// Update the password from the dialog
@ -102,6 +101,7 @@ enum WiFiDialog {
ssid: network_manager::SSID,
identity: Option<String>,
password: SecureString,
network_type: NetworkType,
password_hidden: bool,
tx: SecretSender,
},
@ -141,7 +141,7 @@ pub struct Page {
#[derive(Debug)]
pub struct NmState {
conn: zbus::Connection,
conn: nmrs::NetworkManager,
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
state: network_manager::NetworkManagerState,
devices: Vec<network_manager::devices::DeviceInfo>,
@ -294,19 +294,23 @@ impl page::Page<crate::pages::Message> for Page {
let (tx, rx) = tokio::sync::mpsc::channel(4);
self.secret_tx = Some(tx);
if self.nm_task.is_none() {
return Task::batch(vec![cosmic::Task::future(async move {
zbus::Connection::system()
.await
.context("failed to create system dbus connection")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
)
.apply(crate::pages::Message::WiFi)
}), cosmic::Task::stream(
cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", rx),
)
.map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m)))]);
return Task::batch(vec![
cosmic::Task::future(async move {
nmrs::NetworkManager::new()
.await
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
)
.apply(crate::pages::Message::WiFi)
}),
cosmic::Task::stream(nm_secret_agent::secret_agent_stream(
"com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent",
rx,
))
.map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m))),
]);
}
Task::none()
@ -352,15 +356,22 @@ impl Page {
}
match req {
network_manager::Request::Authenticate { ssid, identity, .. } => {
network_manager::Request::Authenticate {
ssid,
identity,
network_type,
..
} => {
if success {
self.connecting.remove(ssid.as_str());
} else {
self.connecting.remove(ssid.as_str());
// Request to retry
self.dialog = Some(WiFiDialog::Password {
ssid: ssid.into(),
identity,
password: SecureString::from(""),
network_type,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
@ -377,11 +388,13 @@ impl Page {
if success || matches!(network_type, NetworkType::Open) {
self.connecting.remove(ssid.as_ref());
} else {
self.connecting.remove(ssid.as_ref());
self.dialog = Some(WiFiDialog::Password {
ssid,
identity: matches!(network_type, NetworkType::EAP)
.then(String::new),
password: SecureString::from(""),
network_type,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
@ -449,6 +462,7 @@ impl Page {
let qr_string = if let Some(ref pass) = password {
let security = match security_type {
NetworkType::PskOrSae => "WPA",
NetworkType::Sae => "WPA",
NetworkType::EAP => "WPA",
NetworkType::Open => "",
};
@ -521,6 +535,7 @@ impl Page {
ssid,
identity: matches!(ap.network_type, NetworkType::EAP).then(String::new),
password: SecureString::from(""),
network_type: ap.network_type,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
@ -544,6 +559,7 @@ impl Page {
ssid,
identity,
password,
network_type,
tx,
..
} = dialog
@ -562,6 +578,7 @@ impl Page {
ssid: ssid.to_string(),
identity,
password,
network_type,
secret_tx,
interface,
});
@ -614,6 +631,7 @@ impl Page {
}
Message::Disconnect(ssid) => {
self.close_popup_and_apply_updates();
self.connecting.remove(ssid.as_ref());
if let Some(nm) = self.nm_state.as_mut() {
_ = nm
.sender
@ -627,6 +645,7 @@ impl Page {
Message::Forget(ssid) => {
self.dialog = None;
self.close_popup_and_apply_updates();
self.connecting.remove(ssid.as_ref());
if let Some(nm) = self.nm_state.as_mut() {
_ = nm
.sender
@ -648,6 +667,9 @@ impl Page {
}
}
Message::WiFiEnable(enable) => {
if !enable {
self.connecting.clear();
}
if let Some(nm) = self.nm_state.as_mut() {
_ = nm
.sender
@ -656,7 +678,9 @@ impl Page {
}
}
Message::CancelDialog => {
self.dialog = None;
if let Some(WiFiDialog::Password { ssid, .. }) = self.dialog.take() {
self.connecting.remove(ssid.as_ref());
}
}
Message::Error(why) => {
tracing::error!(why);
@ -715,15 +739,20 @@ impl Page {
password: previous,
password_hidden: true,
identity: matches!(ap.network_type, NetworkType::EAP).then(String::new),
network_type: ap.network_type,
tx,
});
return task::message(Message::FocusSecureInput);
}
nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => {
self.dialog = self
.dialog
.take()
.filter(|d| !matches!(d, &WiFiDialog::Password { .. }));
match self.dialog.take() {
Some(WiFiDialog::Password { ssid, .. }) => {
self.connecting.remove(ssid.as_ref());
}
other => {
self.dialog = other;
}
}
}
nm_secret_agent::Event::Failed(error) => {
tracing::error!(%error, "secret agent failure");
@ -731,15 +760,18 @@ impl Page {
ssid,
password,
identity,
network_type,
..
}) = self.dialog.take()
{
self.connecting.remove(ssid.as_ref());
self.dialog = Some(WiFiDialog::Password {
password,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
ssid,
identity,
network_type,
});
return task::message(Message::FocusSecureInput);
}
@ -770,19 +802,14 @@ impl Page {
Task::none()
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let watchers = std::pin::pin!(async move {
futures::join!(
network_manager::watch(conn.clone(), tx.clone()),
network_manager::active_conns::watch(conn.clone(), tx.clone()),
network_manager::wireless_enabled::watch(conn.clone(), tx.clone()),
network_manager::watch_connections_changed(conn, tx)
);
network_manager::watch(conn, tx).await;
});
let forwarder = std::pin::pin!(async move {
@ -832,6 +859,12 @@ impl Page {
/// Withholds updates if the view more popup is displayed.
fn update_state(&mut self, state: NetworkManagerState) {
for active in &state.active_conns {
if let ActiveConnectionInfo::WiFi { name, .. } = active {
self.connecting.remove(name.as_str());
}
}
if let Some(ref mut nm_state) = self.nm_state {
if self.view_more_popup.is_some() {
self.withheld_state = Some(state);
@ -1271,51 +1304,9 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
.into()
}
fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
let settings = async move {
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
_ = settings.load_connections(&[]).await;
let settings = settings
// Get a list of known connections.
.list_connections()
.await?
// Prepare for wrapping in a concurrent stream.
.into_iter()
.map(|conn| async move { conn })
// Create a concurrent stream for each connection.
.apply(futures::stream::FuturesOrdered::from_iter)
// Concurrently fetch settings for each connection.
.filter_map(|conn| async move {
conn.get_settings()
.await
.map(network_manager::Settings::new)
.ok()
})
// Reduce the settings list into a SSID->UUID map.
.fold(BTreeMap::new(), |mut set, settings| async move {
if let Some(ref wifi) = settings.wifi
&& let Some(ssid) = wifi
.ssid
.clone()
.and_then(|ssid| String::from_utf8(ssid).ok())
&& let Some(ref connection) = settings.connection
&& let Some(uuid) = connection.uuid.clone()
{
set.insert(ssid.into(), uuid.into());
return set;
}
set
})
.await;
Ok::<_, zbus::Error>(settings)
};
fn connection_settings(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
settings
network_manager::wifi_connection_settings(conn)
.await
.context("failed to get connection settings")
.map_or_else(
@ -1326,7 +1317,7 @@ fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
pub fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
pub fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
@ -1335,7 +1326,7 @@ pub fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
pub fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
pub fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi);

View file

@ -10,12 +10,14 @@ use cosmic::iced::{Alignment, Length};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::widget::{self, icon};
use cosmic::{Apply, Element, Task};
use cosmic_dbus_networkmanager::interface::enums::DeviceState;
use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo;
use cosmic_settings_network_manager_subscription::{self as network_manager, NetworkManagerState};
use cosmic_settings_page::{self as page, Section, section};
use futures::{SinkExt, StreamExt};
use super::backend as network_manager;
use super::backend::NetworkManagerState;
use super::backend::current_networks::ActiveConnectionInfo;
use super::backend::devices::DeviceState;
pub type ConnectionId = Arc<str>;
#[derive(Clone, Debug)]
@ -32,8 +34,8 @@ pub enum Message {
Error(String),
/// An update from the network manager daemon
NetworkManager(network_manager::Event),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Refresh devices and their connection profiles
Refresh,
/// Create a dialog to ask for confirmation of removal.
@ -91,7 +93,7 @@ pub struct Page {
#[derive(Debug)]
pub struct NmState {
conn: zbus::Connection,
conn: nmrs::NetworkManager,
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
active_conns: Vec<ActiveConnectionInfo>,
devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
@ -154,9 +156,9 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
if self.nm_task.is_none() {
return cosmic::task::future(async move {
zbus::Connection::system()
nmrs::NetworkManager::new()
.await
.context("failed to create system dbus connection")
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
@ -356,18 +358,14 @@ impl Page {
Task::none()
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let watchers = std::pin::pin!(async move {
futures::join!(
network_manager::watch(conn.clone(), tx.clone()),
network_manager::active_conns::watch(conn.clone(), tx.clone()),
network_manager::devices::watch(conn, true, tx)
)
network_manager::watch(conn, tx).await;
});
let forwarder = std::pin::pin!(async move {
@ -622,7 +620,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
.into()
}
fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
@ -631,7 +629,7 @@ fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet);