diff --git a/cosmic-applet-network/i18n/en/cosmic_applet_network.ftl b/cosmic-applet-network/i18n/en/cosmic_applet_network.ftl index fbda56d0..30812895 100644 --- a/cosmic-applet-network/i18n/en/cosmic_applet_network.ftl +++ b/cosmic-applet-network/i18n/en/cosmic_applet_network.ftl @@ -3,6 +3,7 @@ airplane-mode = Airplane mode airplane-mode-on = Airplane mode is on turn-off-airplane-mode = Turn off to enable Wi-Fi, Bluetooth and mobile broadband. wifi = Wi-Fi +identity = Identity ipv4 = IPv4 address ipv6 = IPv6 address mac = MAC diff --git a/cosmic-applet-network/src/app.rs b/cosmic-applet-network/src/app.rs index a67b6195..de6c9b2b 100644 --- a/cosmic-applet-network/src/app.rs +++ b/cosmic-applet-network/src/app.rs @@ -34,11 +34,14 @@ use zbus::Connection; use crate::{ config, fl, network_manager::{ - active_conns::active_conns_subscription, available_wifi::AccessPoint, - current_networks::ActiveConnectionInfo, devices::devices_subscription, - hw_address::HwAddress, network_manager_subscription, - wireless_enabled::wireless_enabled_subscription, NetworkManagerEvent, - NetworkManagerRequest, NetworkManagerState, + active_conns::active_conns_subscription, + available_wifi::{AccessPoint, NetworkType}, + current_networks::ActiveConnectionInfo, + devices::devices_subscription, + hw_address::HwAddress, + network_manager_subscription, + wireless_enabled::wireless_enabled_subscription, + NetworkManagerEvent, NetworkManagerRequest, NetworkManagerState, }, }; @@ -50,6 +53,7 @@ pub fn run() -> cosmic::iced::Result { enum NewConnectionState { EnterPassword { access_point: AccessPoint, + identity: String, password: String, password_hidden: bool, }, @@ -235,6 +239,7 @@ pub(crate) enum Message { SelectWirelessAccessPoint(AccessPoint), CancelNewConnection, Password(String), + Identity(String), SubmitPassword, Frame(Instant), Token(TokenUpdate), @@ -331,41 +336,57 @@ impl cosmic::Application for CosmicNetworkApplet { success, req, } => { - if let NetworkManagerRequest::SelectAccessPoint(ssid, hw_address) = &req { - let conn_match = self - .new_connection - .as_ref() - .map(|c| c.ssid() == ssid && c.hw_address() == *hw_address) - .unwrap_or_default(); - if conn_match && success { - if let Some(s) = - state.active_conns.iter_mut().find(|ap| &ap.name() == ssid && ap.hw_address() == *hw_address) - { - match s { - ActiveConnectionInfo::WiFi { state, .. } => { - *state = ActiveConnectionState::Activated; - } - _ => {} - }; + if let NetworkManagerRequest::SelectAccessPoint( + ssid, + hw_address, + _network_type, + ) = &req + { + let conn_match = self + .new_connection + .as_ref() + .map(|c| c.ssid() == ssid && c.hw_address() == *hw_address) + .unwrap_or_default(); + + if conn_match && success { + if let Some(s) = state + .active_conns + .iter_mut() + .find(|ap| &ap.name() == ssid && ap.hw_address() == *hw_address) + { + match s { + ActiveConnectionInfo::WiFi { state, .. } => { + *state = ActiveConnectionState::Activated; } - self.failed_known_ssids.remove(ssid); - self.new_connection = None; - self.show_visible_networks = false; - } else if !matches!( - &self.new_connection, - Some(NewConnectionState::EnterPassword { .. }) - ) - { - self.failed_known_ssids.insert(ssid.clone()); - } - } else if let NetworkManagerRequest::Password(ssid, _, hw_address) = &req { - if let Some(NewConnectionState::Waiting(access_point)) = - self.new_connection.clone() - { - if !success && ssid == &access_point.ssid && *hw_address == access_point.hw_address { - self.new_connection = - Some(NewConnectionState::Failure(access_point.clone())); - } else { + _ => {} + }; + } + self.failed_known_ssids.remove(ssid); + self.new_connection = None; + self.show_visible_networks = false; + } else if !matches!( + &self.new_connection, + Some(NewConnectionState::EnterPassword { .. }) + ) { + self.failed_known_ssids.insert(ssid.clone()); + } + } else if let NetworkManagerRequest::Authenticate { + ssid, + identity: _, + password: _, + hw_address, + } = &req + { + if let Some(NewConnectionState::Waiting(access_point)) = + self.new_connection.clone() + { + if !success + && ssid == &access_point.ssid + && *hw_address == access_point.hw_address + { + self.new_connection = + Some(NewConnectionState::Failure(access_point.clone())); + } else { self.new_connection = None; self.show_visible_networks = false; } @@ -412,13 +433,19 @@ impl cosmic::Application for CosmicNetworkApplet { let _ = tx.unbounded_send(NetworkManagerRequest::SelectAccessPoint( access_point.ssid.clone(), access_point.hw_address, + access_point.network_type, )); - self.new_connection = Some(NewConnectionState::EnterPassword { - access_point, - password: String::new(), - password_hidden: true, - }); + if matches!(access_point.network_type, NetworkType::Open) { + self.new_connection = Some(NewConnectionState::Waiting(access_point)); + } else { + self.new_connection = Some(NewConnectionState::EnterPassword { + access_point, + identity: String::new(), + password: String::new(), + password_hidden: true, + }); + } } Message::ToggleVisibleNetworks => { self.new_connection = None; @@ -450,19 +477,24 @@ impl cosmic::Application for CosmicNetworkApplet { if let Some(NewConnectionState::EnterPassword { password, access_point, + identity, .. }) = self.new_connection.take() { - let _ = tx.unbounded_send(NetworkManagerRequest::Password( - access_point.ssid.clone(), - password, - access_point.hw_address, - )); + let is_enterprise: bool = matches!(access_point.network_type, NetworkType::EAP); + + let _ = tx.unbounded_send(NetworkManagerRequest::Authenticate { + ssid: access_point.ssid.clone(), + identity: is_enterprise.then(|| identity.clone()), + password: password, + hw_address: access_point.hw_address, + }); self.new_connection .replace(NewConnectionState::Waiting(access_point)); }; } Message::ActivateKnownWifi(ssid, hw_address) => { + let mut network_type = NetworkType::Open; let tx = if let Some(tx) = self.nm_sender.as_ref() { if let Some(ap) = self .nm_state @@ -470,14 +502,18 @@ impl cosmic::Application for CosmicNetworkApplet { .iter_mut() .find(|c| c.ssid == ssid && c.hw_address == hw_address) { + network_type = ap.network_type; ap.working = true; } tx } else { return Task::none(); }; - let _ = - tx.unbounded_send(NetworkManagerRequest::SelectAccessPoint(ssid, hw_address)); + let _ = tx.unbounded_send(NetworkManagerRequest::SelectAccessPoint( + ssid, + hw_address, + network_type, + )); } Message::CancelNewConnection => { self.new_connection = None; @@ -570,6 +606,13 @@ impl cosmic::Application for CosmicNetworkApplet { cosmic::app::Action::Surface(a), )); } + Message::Identity(new_identity) => { + if let Some(NewConnectionState::EnterPassword { identity, .. }) = + &mut self.new_connection + { + *identity = new_identity; + } + } } Task::none() } @@ -964,6 +1007,7 @@ impl cosmic::Application for CosmicNetworkApplet { match new_conn_state { NewConnectionState::EnterPassword { access_point, + identity, password, password_hidden, } => { @@ -978,30 +1022,38 @@ impl cosmic::Application for CosmicNetworkApplet { .spacing(12), ); content = content.push(id); - let enter_password_col = column![ - text::body(fl!("enter-password")), - text_input::secure_input( - "", - password, - Some(Message::TogglePasswordVisibility), - *password_hidden, + + let is_enterprise = matches!(access_point.network_type, NetworkType::EAP); + let enter_password_col = column![] + .push_maybe(is_enterprise.then(|| text::body(fl!("identity")))) + .push_maybe(is_enterprise.then(|| { + text_input::text_input("", identity).on_input(Message::Identity) + })) + .push(text::body(fl!("enter-password"))) + .push( + text_input::secure_input( + "", + password, + Some(Message::TogglePasswordVisibility), + *password_hidden, + ) + .on_input(Message::Password) + .on_paste(Message::Password) + .on_submit(|_| Message::SubmitPassword), ) - .on_input(Message::Password) - .on_paste(Message::Password) - .on_submit(|_| Message::SubmitPassword) - ] - .push_maybe( - access_point - .wps_push - .then(|| container(text::body(fl!("router-wps-button"))).padding(8)), - ) - .push( - row![ - button::standard(fl!("cancel")).on_press(Message::CancelNewConnection), - button::suggested(fl!("connect")).on_press(Message::SubmitPassword) - ] - .spacing(24), - ); + .push_maybe( + access_point.wps_push.then(|| { + container(text::body(fl!("router-wps-button"))).padding(8) + }), + ) + .push( + row![ + button::standard(fl!("cancel")) + .on_press(Message::CancelNewConnection), + button::suggested(fl!("connect")).on_press(Message::SubmitPassword) + ] + .spacing(24), + ); let col = padded_control(enter_password_col.spacing(8).align_x(Alignment::Center)) .align_x(Alignment::Center); diff --git a/cosmic-applet-network/src/network_manager/available_wifi.rs b/cosmic-applet-network/src/network_manager/available_wifi.rs index 57d06ea8..ac78f318 100644 --- a/cosmic-applet-network/src/network_manager/available_wifi.rs +++ b/cosmic-applet-network/src/network_manager/available_wifi.rs @@ -2,7 +2,10 @@ use cosmic_dbus_networkmanager::{ device::wireless::WirelessDevice, - interface::enums::{ApFlags, DeviceState}, + interface::{ + access_point::AccessPointProxy, + enums::{ApFlags, ApSecurityFlags, DeviceState}, + }, }; use futures_util::StreamExt; @@ -42,7 +45,22 @@ pub async fn handle_wireless_device( 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 { @@ -56,6 +74,7 @@ pub async fn handle_wireless_device( .and_then(|str_addr| HwAddress::from_str(str_addr)) .unwrap_or_default(), wps_push, + network_type, }, ); } @@ -75,4 +94,15 @@ pub struct AccessPoint { 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 +#[derive(Debug, Clone, Copy)] +pub enum NetworkType { + Open, + PSK, + EAP, } diff --git a/cosmic-applet-network/src/network_manager/mod.rs b/cosmic-applet-network/src/network_manager/mod.rs index 295b03e4..5759ad2f 100644 --- a/cosmic-applet-network/src/network_manager/mod.rs +++ b/cosmic-applet-network/src/network_manager/mod.rs @@ -7,6 +7,7 @@ pub mod wireless_enabled; use std::{collections::HashMap, fmt::Debug, time::Duration}; +use available_wifi::NetworkType; use cosmic::{ iced::{self, Subscription}, iced_futures::stream, @@ -186,39 +187,52 @@ async fn start_listening( }; _ = output.send(response).await; } - Some(NetworkManagerRequest::Password(ssid, password, hw_address)) => { + Some(NetworkManagerRequest::Authenticate { + ssid, + identity, + password, + hw_address, + }) => { let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - let success = nm_state - .connect_wifi(&conn, &ssid, Some(&password), hw_address) - .await - .is_ok(); - - let status = Some(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::Password( - ssid.clone(), - password.clone(), + let mut success = true; + let err = nm_state + .connect_wifi( + &conn, + &ssid, + identity.as_deref(), + Some(&password), hw_address, - ), - success, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }); + ) + .await; - if let Some(e) = status { - _ = output.send(e).await; - } else { - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::Password(ssid, password, hw_address), - success: false, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }) - .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)) => { + Some(NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type)) => { + // wait for identity before attempting to connect. + if !matches!(network_type, NetworkType::Open) { + return State::Waiting(conn, rx); + } let state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - let success = if let Err(err) = - state.connect_wifi(&conn, &ssid, None, hw_address).await + + let success = if let Err(err) = state + .connect_wifi(&conn, &ssid, None, None, hw_address) + .await { tracing::error!("Failed to connect to access point: {:?}", err); false @@ -228,7 +242,11 @@ async fn start_listening( _ = output .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::SelectAccessPoint(ssid.clone(), hw_address), + req: NetworkManagerRequest::SelectAccessPoint( + ssid.clone(), + hw_address, + network_type, + ), success, state: NetworkManagerState::new(&conn).await.unwrap_or_default(), }) @@ -287,9 +305,14 @@ async fn start_listening( pub enum NetworkManagerRequest { SetAirplaneMode(bool), SetWiFi(bool), - SelectAccessPoint(String, HwAddress), + SelectAccessPoint(String, HwAddress, NetworkType), Disconnect(String, HwAddress), - Password(String, String, HwAddress), + Authenticate { + ssid: String, + identity: Option, + password: String, + hw_address: HwAddress, + }, Forget(String, HwAddress), Reload, } @@ -415,6 +438,15 @@ impl NetworkManagerState { .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?; @@ -433,6 +465,7 @@ impl NetworkManagerState { &self, conn: &Connection, ssid: &str, + identity: Option<&str>, password: Option<&str>, hw_address: HwAddress, ) -> anyhow::Result<()> { @@ -470,7 +503,26 @@ impl NetworkManagerState { ), ]); - if let Some(pass) = password { + if let Some(identity) = identity { + conn_settings.insert( + "802-1x", + HashMap::from([ + ("identity", Value::Str(identity.into())), + // most common default + ("eap", Value::Array(vec!["peap"].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([ @@ -543,30 +595,31 @@ impl NetworkManagerState { .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 state = - enums::ActiveConnectionState::from(active_conn.state().await.unwrap_or_default()); - return match state { - ActiveConnectionState::Activating => { - if let Ok(Some(s)) = tokio::time::timeout( - Duration::from_secs(20), - active_conn.receive_state_changed().await.next(), - ) - .await - { - state = s.get().await.unwrap_or_default().into(); - if matches!(state, enums::ActiveConnectionState::Activated) { - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to activate connection")) - } - } else { - Err(anyhow::anyhow!("Failed to activate connection")) - } + 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"); } - ActiveConnectionState::Activated => Ok(()), - _ => Err(anyhow::anyhow!("Failed to activate connection")), - }; + match tokio::time::timeout(Duration::from_secs(20), changes.next()).await { + Ok(Some(s)) => { + 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"))