refactor: use settings subscriptions and nm secret agent
This commit is contained in:
parent
6534939ba6
commit
10e4f84d3f
14 changed files with 1184 additions and 1742 deletions
210
Cargo.lock
generated
210
Cargo.lock
generated
|
|
@ -104,6 +104,17 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
|
|
@ -336,6 +347,17 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fn-stream"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4ba0c4baf81a0d8ab31618ffa3ae29ceeb970a6d0d82f76130753462e39d0ea"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "1.13.0"
|
||||
|
|
@ -582,7 +604,7 @@ dependencies = [
|
|||
"bitflags 2.10.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"itertools 0.13.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
|
|
@ -636,6 +658,15 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
|
|
@ -827,6 +858,15 @@ dependencies = [
|
|||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.51"
|
||||
|
|
@ -906,6 +946,16 @@ dependencies = [
|
|||
"phf 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
|
|
@ -1273,19 +1323,26 @@ name = "cosmic-applet-network"
|
|||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-fn-stream",
|
||||
"cosmic-dbus-networkmanager",
|
||||
"cosmic-settings-airplane-mode-subscription",
|
||||
"cosmic-settings-network-manager-subscription",
|
||||
"cosmic-time",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"indexmap 2.12.1",
|
||||
"libcosmic",
|
||||
"nm-secret-agent-manager",
|
||||
"rust-embed",
|
||||
"rustc-hash 2.1.1",
|
||||
"secure-string",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"zbus 5.12.0",
|
||||
]
|
||||
|
||||
|
|
@ -1625,6 +1682,18 @@ dependencies = [
|
|||
"zbus 5.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-settings-airplane-mode-subscription"
|
||||
version = "1.0.0-beta6"
|
||||
source = "git+https://github.com/pop-os/cosmic-settings/?branch=nm-secret-agent#8bcec57132e0610cd7630b04c9be5e0661863b73"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_futures",
|
||||
"log",
|
||||
"rustix 1.1.3",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-settings-config"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1659,6 +1728,25 @@ dependencies = [
|
|||
"zbus 5.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-settings-network-manager-subscription"
|
||||
version = "1.0.0-beta6"
|
||||
source = "git+https://github.com/pop-os/cosmic-settings/?branch=nm-secret-agent#8bcec57132e0610cd7630b04c9be5e0661863b73"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cosmic-dbus-networkmanager",
|
||||
"futures",
|
||||
"iced_futures",
|
||||
"itertools 0.14.0",
|
||||
"nm-secret-agent-manager",
|
||||
"secret-service",
|
||||
"secure-string",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"zbus 5.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-settings-sound-subscription"
|
||||
version = "1.0.0-beta6"
|
||||
|
|
@ -2029,6 +2117,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2968,6 +3057,24 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i18n-config"
|
||||
version = "0.4.8"
|
||||
|
|
@ -3775,6 +3882,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input"
|
||||
version = "0.9.1"
|
||||
|
|
@ -3848,6 +3965,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.16"
|
||||
|
|
@ -4510,6 +4636,13 @@ dependencies = [
|
|||
"memoffset 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nm-secret-agent-manager"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zbus 5.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
@ -4562,6 +4695,20 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
|
|
@ -4572,6 +4719,15 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
|
|
@ -4598,6 +4754,17 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
|
|
@ -5915,6 +6082,35 @@ dependencies = [
|
|||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secret-service"
|
||||
version = "5.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cbc",
|
||||
"futures-util",
|
||||
"generic-array",
|
||||
"getrandom 0.2.16",
|
||||
"hkdf",
|
||||
"num",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"sha2",
|
||||
"zbus 5.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secure-string"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "548ba8c9ff631f7bb3a64de1e8ad73fe20f6d04090724f2b496ed45314ad7488"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.2.1"
|
||||
|
|
@ -6320,6 +6516,12 @@ dependencies = [
|
|||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "svg_fmt"
|
||||
version = "0.4.5"
|
||||
|
|
@ -8337,6 +8539,12 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
|
|
|
|||
|
|
@ -88,3 +88,8 @@ sctk = { package = "smithay-client-toolkit", version = "0.20.0" }
|
|||
[patch."https://github.com/pop-os/cosmic-protocols"]
|
||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
||||
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
||||
|
||||
[patch.'https://github.com/pop-os/dbus-settings-bindings']
|
||||
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
|
||||
# upower_dbus = { path = "../dbus-settings-bindings/upower" }
|
||||
nm-secret-agent-manager = { path = "../dbus-settings-bindings/nm-secret-agent-manager" }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ license = "GPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-fn-stream = "0.3"
|
||||
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||
cosmic-time.workspace = true
|
||||
futures.workspace = true
|
||||
|
|
@ -26,3 +27,17 @@ tracing-log.workspace = true
|
|||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
zbus.workspace = true
|
||||
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings/", branch = "nm-secret-agent" }
|
||||
indexmap = "2.12.1"
|
||||
secure-string = "0.3.0"
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
|
||||
|
||||
[dependencies.cosmic-settings-network-manager-subscription]
|
||||
git = "https://github.com/pop-os/cosmic-settings/"
|
||||
branch = "nm-secret-agent"
|
||||
|
||||
|
||||
[dependencies.cosmic-settings-airplane-mode-subscription]
|
||||
git = "https://github.com/pop-os/cosmic-settings/"
|
||||
branch = "nm-secret-agent"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@
|
|||
mod app;
|
||||
mod config;
|
||||
mod localize;
|
||||
mod network_manager;
|
||||
mod utils;
|
||||
|
||||
use crate::localize::localize;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
use super::{NetworkManagerEvent, NetworkManagerState};
|
||||
use cosmic::{
|
||||
iced::{self, Subscription},
|
||||
iced_futures::stream,
|
||||
};
|
||||
use cosmic_dbus_networkmanager::nm::NetworkManager;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
use zbus::Connection;
|
||||
|
||||
pub fn active_conns_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
conn: Connection,
|
||||
) -> iced::Subscription<NetworkManagerEvent> {
|
||||
let initial = State::Continue(conn);
|
||||
Subscription::run_with_id(
|
||||
id,
|
||||
stream::channel(50, move |mut output| {
|
||||
let mut state = initial;
|
||||
|
||||
async move {
|
||||
loop {
|
||||
state = start_listening(state, &mut output).await;
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum State {
|
||||
Continue(Connection),
|
||||
Error,
|
||||
}
|
||||
|
||||
async fn start_listening(
|
||||
state: State,
|
||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
||||
) -> State {
|
||||
let conn = match state {
|
||||
State::Continue(conn) => conn,
|
||||
State::Error => iced::futures::future::pending().await,
|
||||
};
|
||||
let network_manager = match NetworkManager::new(&conn).await {
|
||||
Ok(n) => n,
|
||||
Err(why) => {
|
||||
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
|
||||
return State::Error;
|
||||
}
|
||||
};
|
||||
|
||||
let mut active_conns_changed = network_manager.receive_active_connections_changed().await;
|
||||
active_conns_changed.next().await;
|
||||
|
||||
while let (Some(_change), ()) = tokio::join!(
|
||||
active_conns_changed.next(),
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1))
|
||||
) {
|
||||
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::ActiveConns(new_state))
|
||||
.await;
|
||||
}
|
||||
|
||||
State::Continue(conn)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use cosmic_dbus_networkmanager::settings::{NetworkManagerSettings, connection::Settings};
|
||||
use zbus::Connection;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VpnConnection {
|
||||
pub name: String,
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
/// Load all available VPN connections from NetworkManager settings
|
||||
pub async fn load_vpn_connections(conn: &Connection) -> anyhow::Result<Vec<VpnConnection>> {
|
||||
let nm_settings = NetworkManagerSettings::new(conn).await?;
|
||||
let connections = nm_settings.list_connections().await?;
|
||||
|
||||
let mut vpn_connections = Vec::new();
|
||||
|
||||
for connection in connections {
|
||||
let settings_map = match connection.get_settings().await {
|
||||
Ok(s) => s,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let settings = Settings::new(settings_map);
|
||||
|
||||
// Check if this is a VPN connection
|
||||
if let Some(connection_settings) = &settings.connection {
|
||||
if let Some(conn_type) = &connection_settings.type_ {
|
||||
// VPN connections have type "vpn" or "wireguard"
|
||||
if conn_type == "vpn" || conn_type == "wireguard" {
|
||||
let name = connection_settings
|
||||
.id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Unknown VPN".to_string());
|
||||
let uuid = connection_settings.uuid.clone().unwrap_or_default();
|
||||
|
||||
vpn_connections.push(VpnConnection { name, uuid });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name for consistent UI
|
||||
vpn_connections.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(vpn_connections)
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use cosmic_dbus_networkmanager::{
|
||||
device::wireless::WirelessDevice,
|
||||
interface::{
|
||||
access_point::AccessPointProxy,
|
||||
enums::{ApFlags, ApSecurityFlags, DeviceState},
|
||||
},
|
||||
};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::HashMap;
|
||||
use zbus::zvariant::ObjectPath;
|
||||
|
||||
use super::hw_address::HwAddress;
|
||||
|
||||
pub async fn handle_wireless_device(
|
||||
device: WirelessDevice<'_>,
|
||||
hw_address: Option<String>,
|
||||
) -> zbus::Result<Vec<AccessPoint>> {
|
||||
device.request_scan(HashMap::new()).await?;
|
||||
let mut scan_changed = device.receive_last_scan_changed().await;
|
||||
if let Some(t) = scan_changed.next().await {
|
||||
if let Ok(-1) = t.get().await {
|
||||
eprintln!("scan errored");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
}
|
||||
let access_points = device.get_access_points().await?;
|
||||
let state: DeviceState = device
|
||||
.upcast()
|
||||
.await
|
||||
.and_then(|dev| dev.cached_state())
|
||||
.unwrap_or_default()
|
||||
.map_or(DeviceState::Unknown, |s| s.into());
|
||||
// Sort by strength and remove duplicates
|
||||
let mut aps = FxHashMap::<String, AccessPoint>::default();
|
||||
for ap in access_points {
|
||||
let ssid = String::from_utf8_lossy(ap.ssid().await?.as_slice()).into_owned();
|
||||
let wps_push = ap.flags().await?.contains(ApFlags::WPS_PBC);
|
||||
let strength = ap.strength().await?;
|
||||
if let Some(access_point) = aps.get(&ssid) {
|
||||
if access_point.strength > strength {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let proxy: &AccessPointProxy = ≈
|
||||
let Ok(flags) = ap.rsn_flags().await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let network_type = if flags.intersects(ApSecurityFlags::KEY_MGMT_802_1X) {
|
||||
NetworkType::EAP
|
||||
} else if flags.intersects(ApSecurityFlags::KEY_MGMTPSK) {
|
||||
NetworkType::PSK
|
||||
} else if flags.is_empty() {
|
||||
NetworkType::Open
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
aps.insert(
|
||||
ssid.clone(),
|
||||
AccessPoint {
|
||||
ssid,
|
||||
strength,
|
||||
state,
|
||||
working: false,
|
||||
path: ap.inner().path().to_owned(),
|
||||
hw_address: hw_address
|
||||
.as_ref()
|
||||
.and_then(|str_addr| HwAddress::from_str(str_addr))
|
||||
.unwrap_or_default(),
|
||||
wps_push,
|
||||
network_type,
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut aps = aps.into_values().collect::<Vec<_>>();
|
||||
aps.sort_unstable_by_key(|ap| ap.strength);
|
||||
Ok(aps)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessPoint {
|
||||
pub ssid: String,
|
||||
pub strength: u8,
|
||||
pub state: DeviceState,
|
||||
pub working: bool,
|
||||
pub path: ObjectPath<'static>,
|
||||
pub hw_address: HwAddress,
|
||||
pub wps_push: bool,
|
||||
pub network_type: NetworkType,
|
||||
}
|
||||
|
||||
// TODO do we want to support eap methods other than peap in the applet?
|
||||
// Then we'd need a dropdown for the eap method,
|
||||
// and tls requires a cert instead of a password
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum NetworkType {
|
||||
Open,
|
||||
PSK,
|
||||
EAP,
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use cosmic_dbus_networkmanager::{
|
||||
active_connection::ActiveConnection, device::SpecificDevice,
|
||||
interface::enums::ActiveConnectionState,
|
||||
};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use super::hw_address::HwAddress;
|
||||
|
||||
/// Read network interface speed from sysfs
|
||||
/// Returns speed in Mbps, or None if unable to read
|
||||
fn read_speed_from_sysfs(interface: &str) -> Option<u32> {
|
||||
let path = format!("/sys/class/net/{}/speed", interface);
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|content| content.trim().parse::<i32>().ok())
|
||||
.and_then(|speed| if speed > 0 { Some(speed as u32) } else { None })
|
||||
}
|
||||
|
||||
pub async fn active_connections(
|
||||
active_connections: Vec<ActiveConnection<'_>>,
|
||||
) -> zbus::Result<Vec<ActiveConnectionInfo>> {
|
||||
let mut info = Vec::<ActiveConnectionInfo>::with_capacity(active_connections.len());
|
||||
for connection in active_connections {
|
||||
let ipv4 = connection
|
||||
.ip4_config()
|
||||
.await?
|
||||
.address_data()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let addresses: Vec<_> = ipv4.iter().map(|d| d.address).collect();
|
||||
let state = connection
|
||||
.state()
|
||||
.await
|
||||
.unwrap_or(ActiveConnectionState::Unknown);
|
||||
|
||||
if connection.vpn().await.unwrap_or_default() {
|
||||
info.push(ActiveConnectionInfo::Vpn {
|
||||
name: connection.id().await?,
|
||||
ip_addresses: addresses.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for device in connection.devices().await.unwrap_or_default() {
|
||||
let interface_name = device.interface().await.ok();
|
||||
|
||||
match device
|
||||
.downcast_to_device()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|inner| inner)
|
||||
{
|
||||
Some(SpecificDevice::Wired(wired_device)) => {
|
||||
let mut speed = wired_device.speed().await?;
|
||||
|
||||
// If NetworkManager returns 0, try to read from sysfs
|
||||
if speed == 0 {
|
||||
if let Some(interface) = interface_name.as_ref() {
|
||||
speed = read_speed_from_sysfs(interface).unwrap_or(0);
|
||||
}
|
||||
}
|
||||
|
||||
info.push(ActiveConnectionInfo::Wired {
|
||||
name: connection.id().await?,
|
||||
hw_address: HwAddress::from_str(&wired_device.hw_address().await?)
|
||||
.unwrap_or_default(),
|
||||
speed,
|
||||
ip_addresses: addresses.clone(),
|
||||
});
|
||||
}
|
||||
Some(SpecificDevice::Wireless(wireless_device)) => {
|
||||
if let Ok(access_point) = wireless_device.active_access_point().await {
|
||||
info.push(ActiveConnectionInfo::WiFi {
|
||||
name: String::from_utf8_lossy(&access_point.ssid().await?).into_owned(),
|
||||
ip_addresses: addresses.clone(),
|
||||
hw_address: HwAddress::from_str(&wireless_device.hw_address().await?)
|
||||
.unwrap_or_default(),
|
||||
state,
|
||||
strength: access_point.strength().await.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(SpecificDevice::WireGuard(_)) => {
|
||||
info.push(ActiveConnectionInfo::Vpn {
|
||||
name: connection.id().await?,
|
||||
ip_addresses: addresses.clone(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info.sort_unstable();
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ActiveConnectionInfo {
|
||||
Wired {
|
||||
name: String,
|
||||
hw_address: HwAddress,
|
||||
speed: u32,
|
||||
ip_addresses: Vec<Ipv4Addr>,
|
||||
},
|
||||
WiFi {
|
||||
name: String,
|
||||
ip_addresses: Vec<Ipv4Addr>,
|
||||
hw_address: HwAddress,
|
||||
state: ActiveConnectionState,
|
||||
strength: u8,
|
||||
},
|
||||
Vpn {
|
||||
name: String,
|
||||
ip_addresses: Vec<Ipv4Addr>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ActiveConnectionInfo {
|
||||
pub fn name(&self) -> String {
|
||||
match &self {
|
||||
Self::Wired { name, .. } | Self::WiFi { name, .. } | Self::Vpn { name, .. } => {
|
||||
name.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn hw_address(&self) -> HwAddress {
|
||||
match &self {
|
||||
Self::Wired { hw_address, .. } | Self::WiFi { hw_address, .. } => *hw_address,
|
||||
Self::Vpn { .. } => HwAddress::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::Ord for ActiveConnectionInfo {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(Self::Vpn { .. }, Self::Wired { .. } | Self::WiFi { .. })
|
||||
| (Self::Wired { .. }, Self::WiFi { .. }) => std::cmp::Ordering::Less,
|
||||
|
||||
(Self::WiFi { .. }, Self::Wired { .. } | Self::Vpn { .. })
|
||||
| (Self::Wired { .. }, Self::Vpn { .. }) => std::cmp::Ordering::Greater,
|
||||
|
||||
(Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. })
|
||||
| (Self::Wired { name: n1, .. }, Self::Wired { name: n2, .. })
|
||||
| (Self::WiFi { name: n1, .. }, Self::WiFi { name: n2, .. }) => n1.cmp(n2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::Eq for ActiveConnectionInfo {}
|
||||
|
||||
impl std::cmp::PartialOrd for ActiveConnectionInfo {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq for ActiveConnectionInfo {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
Self::Wired {
|
||||
name: n1,
|
||||
hw_address: a1,
|
||||
..
|
||||
},
|
||||
Self::Wired {
|
||||
name: n2,
|
||||
hw_address: a2,
|
||||
..
|
||||
},
|
||||
)
|
||||
| (
|
||||
Self::WiFi {
|
||||
name: n1,
|
||||
hw_address: a1,
|
||||
..
|
||||
},
|
||||
Self::WiFi {
|
||||
name: n2,
|
||||
hw_address: a2,
|
||||
..
|
||||
},
|
||||
) => n1 == n2 && a1 == a2,
|
||||
|
||||
(Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. }) => n1 == n2,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
use super::{NetworkManagerEvent, NetworkManagerState};
|
||||
use cosmic::iced::{self, Subscription, stream};
|
||||
use cosmic_dbus_networkmanager::nm::NetworkManager;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
use zbus::Connection;
|
||||
|
||||
pub fn devices_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
has_popup: bool,
|
||||
conn: Connection,
|
||||
) -> iced::Subscription<NetworkManagerEvent> {
|
||||
let initial = State::Continue(conn);
|
||||
Subscription::run_with_id(
|
||||
(id, has_popup),
|
||||
stream::channel(50, move |mut output| {
|
||||
let mut state = initial.clone();
|
||||
|
||||
async move {
|
||||
loop {
|
||||
state = start_listening(state, has_popup, &mut output).await;
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum State {
|
||||
Continue(Connection),
|
||||
Error,
|
||||
}
|
||||
|
||||
async fn start_listening(
|
||||
state: State,
|
||||
has_popup: bool,
|
||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
||||
) -> State {
|
||||
let conn = match state {
|
||||
State::Continue(conn) => conn,
|
||||
State::Error => iced::futures::future::pending().await,
|
||||
};
|
||||
let network_manager = match NetworkManager::new(&conn).await {
|
||||
Ok(n) => n,
|
||||
Err(why) => {
|
||||
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
|
||||
return State::Error;
|
||||
}
|
||||
};
|
||||
|
||||
let mut devices_changed = network_manager.receive_devices_changed().await;
|
||||
|
||||
let secs = if has_popup { 4 } else { 60 };
|
||||
while let (Some(_change), ()) = tokio::join!(
|
||||
devices_changed.next(),
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(secs))
|
||||
) {
|
||||
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::WirelessAccessPoints(new_state))
|
||||
.await;
|
||||
}
|
||||
|
||||
State::Continue(conn)
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)]
|
||||
pub struct HwAddress {
|
||||
address: u64,
|
||||
}
|
||||
|
||||
impl HwAddress {
|
||||
pub fn from_str(arg: &str) -> Option<Self> {
|
||||
let columnless_vec = arg.split(':').collect::<Box<[_]>>();
|
||||
if columnless_vec.len() * 3 - 1 != arg.len() {
|
||||
return None;
|
||||
}
|
||||
for byte in &columnless_vec {
|
||||
if byte.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
u64::from_str_radix(columnless_vec.join("").as_str(), 16)
|
||||
.ok()
|
||||
.map(|address| HwAddress { address })
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HwAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (index, c) in format!("{:x}", self.address).char_indices() {
|
||||
if index != 0 && index % 2 == 0 {
|
||||
f.write_char(':')?;
|
||||
}
|
||||
f.write_char(c)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,822 +0,0 @@
|
|||
pub mod active_conns;
|
||||
pub mod available_vpns;
|
||||
pub mod available_wifi;
|
||||
pub mod current_networks;
|
||||
pub mod devices;
|
||||
pub mod hw_address;
|
||||
pub mod wireless_enabled;
|
||||
|
||||
use std::{collections::HashMap, fmt::Debug, time::Duration};
|
||||
|
||||
use available_wifi::NetworkType;
|
||||
use cosmic::{
|
||||
iced::{self, Subscription},
|
||||
iced_futures::stream,
|
||||
};
|
||||
use cosmic_dbus_networkmanager::{
|
||||
active_connection::ActiveConnection,
|
||||
device::SpecificDevice,
|
||||
interface::{
|
||||
active_connection::ActiveConnectionProxy,
|
||||
enums::{self, ActiveConnectionState, DeviceType, NmConnectivityState},
|
||||
},
|
||||
nm::NetworkManager,
|
||||
settings::{NetworkManagerSettings, connection::Settings},
|
||||
};
|
||||
use futures::{
|
||||
SinkExt, StreamExt,
|
||||
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
|
||||
};
|
||||
use hw_address::HwAddress;
|
||||
use tokio::process::Command;
|
||||
use zbus::{
|
||||
Connection,
|
||||
zvariant::{self, Value},
|
||||
};
|
||||
|
||||
use self::{
|
||||
available_vpns::{VpnConnection, load_vpn_connections},
|
||||
available_wifi::{AccessPoint, handle_wireless_device},
|
||||
current_networks::{ActiveConnectionInfo, active_connections},
|
||||
};
|
||||
|
||||
// In some distros, rfkill is only in sbin, which isn't normally in PATH
|
||||
// TODO: Directly access `/dev/rfkill`
|
||||
fn rfkill_path_var() -> std::ffi::OsString {
|
||||
let mut path = std::env::var_os("PATH").unwrap_or_default();
|
||||
path.push(":/usr/sbin");
|
||||
path
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum State {
|
||||
Ready,
|
||||
Waiting(Connection, UnboundedReceiver<NetworkManagerRequest>),
|
||||
Finished,
|
||||
}
|
||||
|
||||
pub fn network_manager_subscription<I: Copy + Debug + std::hash::Hash + 'static>(
|
||||
id: I,
|
||||
) -> iced::Subscription<NetworkManagerEvent> {
|
||||
Subscription::run_with_id(
|
||||
id,
|
||||
stream::channel(50, |mut output| async move {
|
||||
let mut state = State::Ready;
|
||||
|
||||
loop {
|
||||
state = start_listening(state, &mut output).await;
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn start_listening(
|
||||
state: State,
|
||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
||||
) -> State {
|
||||
match state {
|
||||
State::Ready => {
|
||||
let Ok(conn) = Connection::system().await else {
|
||||
return State::Finished;
|
||||
};
|
||||
|
||||
let (tx, rx) = unbounded();
|
||||
let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
if output
|
||||
.send(NetworkManagerEvent::Init {
|
||||
conn: conn.clone(),
|
||||
sender: tx,
|
||||
state: nm_state,
|
||||
})
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
State::Waiting(conn, rx)
|
||||
} else {
|
||||
State::Finished
|
||||
}
|
||||
}
|
||||
State::Waiting(conn, mut rx) => {
|
||||
let Ok(network_manager) = NetworkManager::new(&conn).await else {
|
||||
return State::Finished;
|
||||
};
|
||||
|
||||
match rx.next().await {
|
||||
Some(NetworkManagerRequest::Disconnect(ssid, hw_address)) => {
|
||||
let mut success = false;
|
||||
for c in network_manager
|
||||
.active_connections()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if c.id().await.unwrap_or_default() != ssid {
|
||||
continue;
|
||||
}
|
||||
let mut is_there_device = false;
|
||||
for device in c.devices().await.unwrap_or_default() {
|
||||
if HwAddress::from_str(device.hw_address().await.as_ref().unwrap())
|
||||
== Some(hw_address)
|
||||
{
|
||||
is_there_device = true;
|
||||
}
|
||||
}
|
||||
|
||||
if is_there_device
|
||||
&& network_manager.deactivate_connection(&c).await.is_ok()
|
||||
{
|
||||
success = true;
|
||||
if let Ok(ActiveConnectionState::Deactivated) = c.state().await {
|
||||
break;
|
||||
} else {
|
||||
let mut changed = c.receive_state_changed().await;
|
||||
_ = tokio::time::timeout(Duration::from_secs(5), async move {
|
||||
loop {
|
||||
if let Some(next) = changed.next().await {
|
||||
if let Ok(ActiveConnectionState::Deactivated) =
|
||||
next.get().await.map(ActiveConnectionState::from)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Disconnect(ssid.clone(), hw_address),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => {
|
||||
// wifi
|
||||
let mut success = network_manager
|
||||
.set_wireless_enabled(!airplane_mode)
|
||||
.await
|
||||
.is_ok();
|
||||
// bluetooth
|
||||
success = success
|
||||
&& Command::new("rfkill")
|
||||
.env("PATH", rfkill_path_var())
|
||||
.arg(if airplane_mode { "block" } else { "unblock" })
|
||||
.arg("bluetooth")
|
||||
.output()
|
||||
.await
|
||||
.is_ok();
|
||||
let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
state.airplane_mode = if success {
|
||||
airplane_mode
|
||||
} else {
|
||||
!airplane_mode
|
||||
};
|
||||
if state.airplane_mode {
|
||||
state.wifi_enabled = false;
|
||||
}
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SetAirplaneMode(airplane_mode),
|
||||
success,
|
||||
state,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Some(NetworkManagerRequest::SetWiFi(enabled)) => {
|
||||
let success = network_manager.set_wireless_enabled(enabled).await.is_ok();
|
||||
let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
state.wifi_enabled = if success { enabled } else { !enabled };
|
||||
let response = NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SetWiFi(enabled),
|
||||
success,
|
||||
state,
|
||||
};
|
||||
_ = output.send(response).await;
|
||||
}
|
||||
Some(NetworkManagerRequest::Authenticate {
|
||||
ssid,
|
||||
identity,
|
||||
password,
|
||||
hw_address,
|
||||
}) => {
|
||||
let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
let mut success = true;
|
||||
let err = nm_state
|
||||
.connect_wifi(
|
||||
&conn,
|
||||
&ssid,
|
||||
identity.as_deref(),
|
||||
Some(&password),
|
||||
hw_address,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = err {
|
||||
success = false;
|
||||
tracing::error!("{:?}", &err);
|
||||
}
|
||||
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Authenticate {
|
||||
ssid: ssid.clone(),
|
||||
identity: identity.clone(),
|
||||
password: password.clone(),
|
||||
hw_address,
|
||||
},
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Some(NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type)) => {
|
||||
if matches!(network_type, NetworkType::Open) {
|
||||
attempt_wifi_connection(&conn, ssid, hw_address, network_type, output)
|
||||
.await;
|
||||
} else {
|
||||
// For secured networks, check if we have saved credentials
|
||||
if !has_saved_wifi_credentials(&conn, &ssid).await {
|
||||
return State::Waiting(conn, rx);
|
||||
}
|
||||
|
||||
// We have saved credentials, attempt connection
|
||||
attempt_wifi_connection(&conn, ssid, hw_address, network_type, output)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Some(NetworkManagerRequest::Reload) => {
|
||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Reload,
|
||||
success: true,
|
||||
state,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Some(NetworkManagerRequest::Forget(ssid, hw_address)) => {
|
||||
let s = NetworkManagerSettings::new(&conn).await.unwrap();
|
||||
let known_conns = s.list_connections().await.unwrap_or_default();
|
||||
let mut success = false;
|
||||
for c in known_conns {
|
||||
let settings = c.get_settings().await.ok().unwrap_or_default();
|
||||
let s = Settings::new(settings);
|
||||
if s.wifi
|
||||
.as_ref()
|
||||
.and_then(|w| w.ssid.as_deref())
|
||||
.is_some_and(|s| std::str::from_utf8(s).is_ok_and(|s| s == ssid))
|
||||
{
|
||||
// todo most likely we can here forget ssid from wrong hw_address
|
||||
_ = c.delete().await;
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Forget(ssid.clone(), hw_address),
|
||||
success,
|
||||
state,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Some(NetworkManagerRequest::ActivateVpn(uuid)) => {
|
||||
tracing::info!("Activating VPN with UUID: {}", uuid);
|
||||
let network_manager = match NetworkManager::new(&conn).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::ActivateVpn(uuid),
|
||||
success: false,
|
||||
state: NetworkManagerState::new(&conn)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.await;
|
||||
return State::Waiting(conn, rx);
|
||||
}
|
||||
};
|
||||
|
||||
let mut success = false;
|
||||
|
||||
// Find the connection by UUID
|
||||
if let Ok(nm_settings) = NetworkManagerSettings::new(&conn).await {
|
||||
if let Ok(connections) = nm_settings.list_connections().await {
|
||||
for connection in connections {
|
||||
if let Ok(settings) = connection.get_settings().await {
|
||||
let settings = Settings::new(settings);
|
||||
if let Some(conn_settings) = &settings.connection {
|
||||
if conn_settings.uuid.as_ref() == Some(&uuid) {
|
||||
// Activate the VPN connection without a specific device
|
||||
// Call the D-Bus method directly since VPNs don't need a device
|
||||
use zbus::zvariant::ObjectPath;
|
||||
let empty_device = ObjectPath::try_from("/").unwrap();
|
||||
|
||||
match network_manager
|
||||
.inner()
|
||||
.call_method(
|
||||
"ActivateConnection",
|
||||
&(
|
||||
connection.inner().path(),
|
||||
empty_device.clone(),
|
||||
empty_device,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!(
|
||||
"Successfully activated VPN: {}",
|
||||
uuid
|
||||
);
|
||||
success = true;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to activate VPN {}: {:?}",
|
||||
uuid,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
tracing::warn!(
|
||||
"VPN connection with UUID {} not found or failed to activate",
|
||||
uuid
|
||||
);
|
||||
}
|
||||
|
||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::ActivateVpn(uuid),
|
||||
success,
|
||||
state,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Some(NetworkManagerRequest::DeactivateVpn(name)) => {
|
||||
tracing::info!("Deactivating VPN: {}", name);
|
||||
let network_manager = match NetworkManager::new(&conn).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::DeactivateVpn(name),
|
||||
success: false,
|
||||
state: NetworkManagerState::new(&conn)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.await;
|
||||
return State::Waiting(conn, rx);
|
||||
}
|
||||
};
|
||||
|
||||
let mut success = false;
|
||||
|
||||
// Find and deactivate the active VPN connection by name
|
||||
if let Ok(active_connections) = network_manager.active_connections().await {
|
||||
for active_conn in active_connections {
|
||||
if let Ok(conn_id) = active_conn.id().await {
|
||||
if conn_id == name && active_conn.vpn().await.unwrap_or(false) {
|
||||
match network_manager.deactivate_connection(&active_conn).await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!(
|
||||
"Successfully deactivated VPN: {}",
|
||||
name
|
||||
);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to deactivate VPN {}: {:?}",
|
||||
name,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
tracing::warn!(
|
||||
"Active VPN connection '{}' not found or failed to deactivate",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::DeactivateVpn(name),
|
||||
success,
|
||||
state,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
_ => {
|
||||
return State::Finished;
|
||||
}
|
||||
}
|
||||
|
||||
State::Waiting(conn, rx)
|
||||
}
|
||||
State::Finished => iced::futures::future::pending().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn has_saved_wifi_credentials(conn: &Connection, ssid: &str) -> bool {
|
||||
let Ok(nm_settings) = NetworkManagerSettings::new(conn).await else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let known_conns = nm_settings.list_connections().await.unwrap_or_default();
|
||||
|
||||
for connection in known_conns {
|
||||
if let Ok(settings) = connection.get_settings().await {
|
||||
let settings = Settings::new(settings);
|
||||
if let Some(saved_ssid) = settings
|
||||
.wifi
|
||||
.and_then(|w| w.ssid)
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok())
|
||||
{
|
||||
if saved_ssid == ssid {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
async fn attempt_wifi_connection(
|
||||
conn: &Connection,
|
||||
ssid: String,
|
||||
hw_address: HwAddress,
|
||||
network_type: NetworkType,
|
||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
||||
) {
|
||||
let state = NetworkManagerState::new(conn).await.unwrap_or_default();
|
||||
|
||||
let success = if let Err(err) = state
|
||||
.connect_wifi(conn, ssid.as_ref(), None, None, hw_address)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to connect to access point: {:?}", err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type),
|
||||
success,
|
||||
state: NetworkManagerState::new(conn).await.unwrap_or_default(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NetworkManagerRequest {
|
||||
SetAirplaneMode(bool),
|
||||
SetWiFi(bool),
|
||||
SelectAccessPoint(String, HwAddress, NetworkType),
|
||||
Disconnect(String, HwAddress),
|
||||
Authenticate {
|
||||
ssid: String,
|
||||
identity: Option<String>,
|
||||
password: String,
|
||||
hw_address: HwAddress,
|
||||
},
|
||||
Forget(String, HwAddress),
|
||||
Reload,
|
||||
ActivateVpn(String), // UUID of VPN connection to activate
|
||||
DeactivateVpn(String), // Name of active VPN connection to deactivate
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NetworkManagerEvent {
|
||||
RequestResponse {
|
||||
req: NetworkManagerRequest,
|
||||
state: NetworkManagerState,
|
||||
success: bool,
|
||||
},
|
||||
Init {
|
||||
conn: Connection,
|
||||
sender: UnboundedSender<NetworkManagerRequest>,
|
||||
state: NetworkManagerState,
|
||||
},
|
||||
WiFiEnabled(NetworkManagerState),
|
||||
WirelessAccessPoints(NetworkManagerState),
|
||||
ActiveConns(NetworkManagerState),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkManagerState {
|
||||
pub wireless_access_points: Vec<AccessPoint>,
|
||||
pub active_conns: Vec<ActiveConnectionInfo>,
|
||||
pub known_access_points: Vec<AccessPoint>,
|
||||
pub available_vpns: Vec<VpnConnection>,
|
||||
pub wifi_enabled: bool,
|
||||
pub airplane_mode: bool,
|
||||
pub connectivity: NmConnectivityState,
|
||||
}
|
||||
|
||||
impl Default for NetworkManagerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wireless_access_points: Vec::new(),
|
||||
active_conns: Vec::new(),
|
||||
known_access_points: Vec::new(),
|
||||
available_vpns: Vec::new(),
|
||||
wifi_enabled: false,
|
||||
airplane_mode: false,
|
||||
connectivity: NmConnectivityState::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkManagerState {
|
||||
pub async fn new(conn: &Connection) -> anyhow::Result<Self> {
|
||||
let network_manager = NetworkManager::new(conn).await?;
|
||||
let mut self_ = Self::default();
|
||||
// airplane mode
|
||||
let airplaine_mode = Command::new("rfkill")
|
||||
.env("PATH", rfkill_path_var())
|
||||
.arg("list")
|
||||
.arg("bluetooth")
|
||||
.output()
|
||||
.await?;
|
||||
let airplane_mode = std::str::from_utf8(&airplaine_mode.stdout).unwrap_or_default();
|
||||
self_.wifi_enabled = network_manager.wireless_enabled().await.unwrap_or_default();
|
||||
self_.airplane_mode = airplane_mode.contains("Soft blocked: yes") && !self_.wifi_enabled;
|
||||
|
||||
let s = NetworkManagerSettings::new(conn).await?;
|
||||
_ = s.load_connections(&[]).await;
|
||||
let known_conns = s.list_connections().await.unwrap_or_default();
|
||||
let active_conns = active_connections(
|
||||
network_manager
|
||||
.active_connections()
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
// active_conns.sort(); active_connections should have already sorted the vector
|
||||
let devices = network_manager.devices().await.ok().unwrap_or_default();
|
||||
let wireless_access_point_futures: Vec<_> = devices
|
||||
.into_iter()
|
||||
.map(|device| async move {
|
||||
if let Ok(Some(SpecificDevice::Wireless(wireless_device))) =
|
||||
device.downcast_to_device().await
|
||||
{
|
||||
handle_wireless_device(wireless_device, device.hw_address().await.ok())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut wireless_access_points = Vec::with_capacity(wireless_access_point_futures.len());
|
||||
for f in wireless_access_point_futures {
|
||||
let mut access_points = f.await;
|
||||
wireless_access_points.append(&mut access_points);
|
||||
}
|
||||
let mut known_ssid = Vec::with_capacity(known_conns.len());
|
||||
for c in known_conns {
|
||||
let Ok(s) = c.get_settings().await else {
|
||||
tracing::info!("Failed to get settings for known connection");
|
||||
continue;
|
||||
};
|
||||
let s = Settings::new(s);
|
||||
if let Some(cur_ssid) = s
|
||||
.wifi
|
||||
.clone()
|
||||
.and_then(|w| w.ssid)
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok())
|
||||
{
|
||||
known_ssid.push(cur_ssid);
|
||||
}
|
||||
}
|
||||
let known_access_points: Vec<_> = wireless_access_points
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
known_ssid.contains(&a.ssid)
|
||||
&& !active_conns
|
||||
.iter()
|
||||
.any(|ac| ac.name() == a.ssid && ac.hw_address() == a.hw_address)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength));
|
||||
self_.wireless_access_points = wireless_access_points;
|
||||
for ap in &self_.wireless_access_points {
|
||||
tracing::info!(
|
||||
"AP ssid: {},\ttype: {:?},\tworking: {},\tstate: {:?}",
|
||||
ap.ssid,
|
||||
ap.network_type,
|
||||
ap.working,
|
||||
ap.state
|
||||
);
|
||||
}
|
||||
self_.active_conns = active_conns;
|
||||
self_.known_access_points = known_access_points;
|
||||
self_.connectivity = network_manager.connectivity().await?;
|
||||
|
||||
// Load available VPN connections
|
||||
self_.available_vpns = load_vpn_connections(conn).await.unwrap_or_default();
|
||||
|
||||
Ok(self_)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn clear(&mut self) {
|
||||
self.active_conns = Vec::new();
|
||||
self.known_access_points = Vec::new();
|
||||
self.wireless_access_points = Vec::new();
|
||||
self.available_vpns = Vec::new();
|
||||
}
|
||||
|
||||
async fn connect_wifi(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
ssid: &str,
|
||||
identity: Option<&str>,
|
||||
password: Option<&str>,
|
||||
hw_address: HwAddress,
|
||||
) -> anyhow::Result<()> {
|
||||
let nm = NetworkManager::new(conn).await?;
|
||||
|
||||
for c in nm.active_connections().await.unwrap_or_default() {
|
||||
if self.wireless_access_points.iter().any(|w| {
|
||||
c.cached_id()
|
||||
.is_ok_and(|opt| opt.is_some_and(|id| id == w.ssid))
|
||||
&& w.hw_address == hw_address
|
||||
}) {
|
||||
_ = nm.deactivate_connection(&c).await;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(ap) = self
|
||||
.wireless_access_points
|
||||
.iter()
|
||||
.find(|ap| ap.ssid == ssid && ap.hw_address == hw_address)
|
||||
else {
|
||||
return Err(anyhow::anyhow!("Access point not found"));
|
||||
};
|
||||
|
||||
let mut conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> = HashMap::from([
|
||||
(
|
||||
"802-11-wireless",
|
||||
HashMap::from([("ssid", Value::Array(ssid.as_bytes().into()))]),
|
||||
),
|
||||
(
|
||||
"connection",
|
||||
HashMap::from([
|
||||
("id", Value::Str(ssid.into())),
|
||||
("type", Value::Str("802-11-wireless".into())),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(identity) = identity {
|
||||
conn_settings.insert(
|
||||
"802-1x",
|
||||
HashMap::from([
|
||||
("identity", Value::Str(identity.into())),
|
||||
// most common default
|
||||
("eap", Value::Array(["peap"].as_slice().into())),
|
||||
// most common default
|
||||
("phase2-auth", Value::Str("mschapv2".into())),
|
||||
("password", Value::Str(password.unwrap_or("").into())),
|
||||
]),
|
||||
);
|
||||
let wireless = conn_settings.get_mut("802-11-wireless").unwrap();
|
||||
wireless.insert("security", Value::Str("802-11-wireless-security".into()));
|
||||
wireless.insert("mode", Value::Str("infrastructure".into()));
|
||||
conn_settings.insert(
|
||||
"802-11-wireless-security",
|
||||
HashMap::from([("key-mgmt", Value::Str("wpa-eap".into()))]),
|
||||
);
|
||||
} else if let Some(pass) = password {
|
||||
conn_settings.insert(
|
||||
"802-11-wireless-security",
|
||||
HashMap::from([
|
||||
("psk", Value::Str(pass.into())),
|
||||
("key-mgmt", Value::Str("wpa-psk".into())),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
let devices = nm.devices().await?;
|
||||
for device in devices {
|
||||
let device_hw_address = device
|
||||
.hw_address()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|device_address| HwAddress::from_str(&device_address))
|
||||
.unwrap_or_default();
|
||||
if device_hw_address != hw_address {
|
||||
continue;
|
||||
}
|
||||
if !matches!(
|
||||
device.device_type().await.unwrap_or(DeviceType::Other),
|
||||
DeviceType::Wifi
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let s = NetworkManagerSettings::new(conn).await?;
|
||||
let known_conns = s.list_connections().await.unwrap_or_default();
|
||||
let mut known_conn = None;
|
||||
for c in known_conns {
|
||||
let settings = c.get_settings().await.ok().unwrap_or_default();
|
||||
|
||||
let s = Settings::new(settings);
|
||||
// todo try to add hw_address comparing here if it changes anything
|
||||
if s.wifi
|
||||
.as_ref()
|
||||
.and_then(|w| w.ssid.as_deref())
|
||||
.is_some_and(|s| std::str::from_utf8(s).is_ok_and(|cur_ssid| cur_ssid == ssid))
|
||||
{
|
||||
known_conn = Some(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let active_conn = if let Some(known_conn) = known_conn.as_ref() {
|
||||
// update settings if needed
|
||||
if password.is_some() {
|
||||
known_conn.update(conn_settings).await?;
|
||||
}
|
||||
|
||||
nm.activate_connection(known_conn, &device).await?
|
||||
} else {
|
||||
let (_, active_conn) = nm
|
||||
.add_and_activate_connection(conn_settings, device.inner().path(), &ap.path)
|
||||
.await?;
|
||||
let dummy = ActiveConnectionProxy::new(conn, active_conn).await?;
|
||||
let active = ActiveConnectionProxy::builder(conn)
|
||||
.destination(dummy.inner().destination().to_owned())
|
||||
.unwrap()
|
||||
.interface(dummy.inner().interface().to_owned())
|
||||
.unwrap()
|
||||
.path(dummy.inner().path().to_owned())
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
ActiveConnection::from(active)
|
||||
};
|
||||
let mut changes = active_conn.receive_state_changed().await;
|
||||
() = tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
let mut count = 5;
|
||||
loop {
|
||||
let state = active_conn.state().await;
|
||||
if let Ok(enums::ActiveConnectionState::Activated) = state {
|
||||
return Ok(());
|
||||
} else if let Ok(enums::ActiveConnectionState::Deactivated) = state {
|
||||
anyhow::bail!("Failed to activate connection");
|
||||
}
|
||||
if let Ok(Some(s)) =
|
||||
tokio::time::timeout(Duration::from_secs(20), changes.next()).await
|
||||
{
|
||||
let state = s.get().await.unwrap_or_default().into();
|
||||
if matches!(state, enums::ActiveConnectionState::Activated) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
count -= 1;
|
||||
if count <= 0 {
|
||||
anyhow::bail!("Failed to activate connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("No wifi device found"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
use super::{NetworkManagerEvent, NetworkManagerState};
|
||||
use cosmic::{
|
||||
iced::{self, Subscription},
|
||||
iced_futures::stream,
|
||||
};
|
||||
use cosmic_dbus_networkmanager::nm::NetworkManager;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
use zbus::Connection;
|
||||
|
||||
pub fn wireless_enabled_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
conn: Connection,
|
||||
) -> iced::Subscription<NetworkManagerEvent> {
|
||||
let initial = State::Continue(conn);
|
||||
Subscription::run_with_id(
|
||||
id,
|
||||
stream::channel(50, move |mut output| {
|
||||
let mut state = initial;
|
||||
|
||||
async move {
|
||||
loop {
|
||||
state = start_listening(state, &mut output).await;
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum State {
|
||||
Continue(Connection),
|
||||
Error,
|
||||
}
|
||||
|
||||
async fn start_listening(
|
||||
state: State,
|
||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
||||
) -> State {
|
||||
let conn = match state {
|
||||
State::Continue(conn) => conn,
|
||||
State::Error => iced::futures::future::pending().await,
|
||||
};
|
||||
|
||||
let network_manager = match NetworkManager::new(&conn).await {
|
||||
Ok(n) => n,
|
||||
Err(why) => {
|
||||
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
|
||||
return State::Error;
|
||||
}
|
||||
};
|
||||
|
||||
let mut wireless_enabled_changed = network_manager.receive_wireless_enabled_changed().await;
|
||||
|
||||
while let Some(_change) = wireless_enabled_changed.next().await {
|
||||
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||
_ = output
|
||||
.send(NetworkManagerEvent::WiFiEnabled(new_state))
|
||||
.await;
|
||||
}
|
||||
State::Continue(conn)
|
||||
}
|
||||
18
cosmic-applet-network/src/utils.rs
Normal file
18
cosmic-applet-network/src/utils.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use futures_util::future::select;
|
||||
|
||||
/// Spawn a background tasks and forward its messages
|
||||
pub fn forward_event_loop<M: 'static + Send, T: Future<Output = ()> + Send + 'static>(
|
||||
event_loop: impl FnOnce(async_fn_stream::StreamEmitter<M>) -> T + Send + 'static,
|
||||
) -> (tokio::sync::oneshot::Sender<()>, cosmic::Task<M>) {
|
||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
let task = cosmic::Task::stream(async_fn_stream::fn_stream(|emitter| async move {
|
||||
select(
|
||||
std::pin::pin!(cancel_rx),
|
||||
std::pin::pin!(event_loop(emitter)),
|
||||
)
|
||||
.await;
|
||||
}));
|
||||
|
||||
(cancel_tx, task)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue