From 35e6a1c9b361f8db3c253cf47af6e921b466be84 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Fri, 7 Nov 2025 17:30:04 -0500 Subject: [PATCH] dbus: saved connections; `forget_btn` thread locks; context correction for network settings; re-introduced EAP --- nmrs-core/src/dbus.rs | 497 +++++++++++++++++++++---- nmrs-core/src/models.rs | 7 + nmrs-core/src/wifi_builders.rs | 122 +++--- nmrs-core/tests/wifi_buillders_test.rs | 13 +- nmrs-ui/src/ui/connect.rs | 37 +- nmrs-ui/src/ui/header.rs | 149 +------- nmrs-ui/src/ui/network_page.rs | 30 +- nmrs-ui/src/ui/networks.rs | 63 +++- 8 files changed, 617 insertions(+), 301 deletions(-) diff --git a/nmrs-core/src/dbus.rs b/nmrs-core/src/dbus.rs index 89054d10..7d0718da 100644 --- a/nmrs-core/src/dbus.rs +++ b/nmrs-core/src/dbus.rs @@ -1,4 +1,6 @@ -use crate::models::{Device, DeviceState, DeviceType, Network, NetworkInfo}; +use crate::models::{ + ConnectionOptions, Device, DeviceState, DeviceType, Network, NetworkInfo, WifiSecurity, +}; use crate::wifi_builders::build_wifi_connection; use futures_timer::Delay; use std::collections::HashMap; @@ -10,7 +12,6 @@ use zvariant::{ObjectPath, OwnedObjectPath}; pub struct NetworkManager { conn: Connection, - cache: tokio::sync::RwLock>, } // Proxies for D-Bus interfaces @@ -33,6 +34,9 @@ trait NMDevice { #[zbus(property)] fn driver(&self) -> Result; + + #[zbus(property)] + fn state_reason(&self) -> Result<(u32, u32)>; } #[proxy( @@ -59,6 +63,13 @@ pub trait NM { specific_object: OwnedObjectPath, ) -> zbus::Result<(OwnedObjectPath, OwnedObjectPath)>; + fn activate_connection( + &self, + connection: OwnedObjectPath, + device: OwnedObjectPath, + specific_object: OwnedObjectPath, + ) -> zbus::Result; + fn deactivate_connection(&self, active_connection: OwnedObjectPath) -> zbus::Result<()>; } @@ -116,10 +127,7 @@ trait NMAccessPoint { impl NetworkManager { pub async fn new() -> zbus::Result { let conn = Connection::system().await?; - Ok(Self { - conn, - cache: tokio::sync::RwLock::new(Vec::new()), - }) + Ok(Self { conn }) } pub async fn list_devices(&self) -> Result> { @@ -155,12 +163,9 @@ impl NetworkManager { pub async fn list_networks(&self) -> Result> { let nm = NMProxy::new(&self.conn).await?; - if self.cache.read().await.is_empty() { - Delay::new(Duration::from_millis(800)).await; - } let devices = nm.get_devices().await?; - let mut networks: HashMap = HashMap::new(); + let mut networks: HashMap<(String, u32), Network> = HashMap::new(); for dp in devices { let d_proxy = NMDeviceProxy::builder(&self.conn) @@ -193,25 +198,26 @@ impl NetworkManager { let flags = ap.flags().await?; let wpa = ap.wpa_flags().await?; let rsn = ap.rsn_flags().await?; + let frequency = ap.frequency().await?; let secured = (flags & 0x1) != 0 || wpa != 0 || rsn != 0; let is_psk = (wpa & 0x0100) != 0 || (rsn & 0x0100) != 0; let is_eap = (wpa & 0x0200) != 0 || (rsn & 0x0200) != 0; - // println!("{} → WPA={wpa:#06x} RSN={rsn:#06x} → EAP={is_eap}", ssid); - let new_net = Network { device: dp.to_string(), ssid: ssid.clone(), bssid: Some(bssid), strength: Some(strength), + frequency: Some(frequency), secured, is_psk, is_eap, }; + // Use (SSID, frequency) as key to separate 2.4GHz and 5GHz networks - .entry(ssid) + .entry((ssid.clone(), frequency)) .and_modify(|n| { if strength > n.strength.unwrap_or(0) { *n = new_net.clone(); @@ -225,18 +231,7 @@ impl NetworkManager { } let result: Vec = networks.into_values().collect(); - - if !result.is_empty() { - println!("cache updates with {} networks", result.len()); - *self.cache.write().await = result.clone(); - } - - if result.is_empty() { - println!("using cached results here"); - Ok(self.cache.read().await.clone()) - } else { - Ok(result) - } + Ok(result) } pub async fn connect(&self, ssid: &str, creds: crate::models::WifiSecurity) -> Result<()> { @@ -250,6 +245,44 @@ impl NetworkManager { let nm = NMProxy::new(&self.conn).await?; + let saved_conn_path = self.get_saved_connection_path(ssid).await?; + + let use_saved_connection = if let Some(conn_path) = &saved_conn_path { + // If PSK is empty, we're trying to use saved credentials + if creds.is_psk() { + if let WifiSecurity::WpaPsk { psk } = &creds { + if psk.trim().is_empty() { + eprintln!("Using saved connection at: {}", conn_path.as_str()); + true + } else { + eprintln!( + "Have saved connection but new password provided, deleting old and creating new" + ); + let _ = self.delete_connection(conn_path.clone()).await; + false + } + } else { + false + } + } else { + // For open or EAP, use saved if available + eprintln!("Using saved connection at: {}", conn_path.as_str()); + true + } + } else { + // No saved connection + if creds.is_psk() + && let WifiSecurity::WpaPsk { psk } = &creds + && psk.trim().is_empty() + { + return Err(zbus::Error::Failure( + "No saved connection and PSK is empty".into(), + )); + } + + false + }; + let devices = nm.get_devices().await?; let mut wifi_device: Option = None; @@ -295,17 +328,15 @@ impl NetworkManager { let state = DeviceState::from(d.state().await?); eprintln!("Loop {i}: Device state = {state:?}"); - if state == DeviceState::Disconnected - || state == DeviceState::Unavailable - || state == DeviceState::Deactivating - { - eprintln!("Device disconneced or deactivating"); + if state == DeviceState::Disconnected || state == DeviceState::Unavailable { + eprintln!("Device disconnected"); break; } Delay::new(Duration::from_millis(300)).await; } + Delay::new(Duration::from_millis(500)).await; eprintln!("Disconnect complete"); } } else { @@ -339,26 +370,228 @@ impl NetworkManager { eprintln!("Could not find AP for '{ssid}'"); } - let settings = build_wifi_connection(ssid, &creds); - let specific_object = ap_path.unwrap_or_else(|| ObjectPath::from_str_unchecked("/").into()); - match nm - .add_and_activate_connection(settings, wifi_device.clone(), specific_object) - .await - { - Ok(_) => eprintln!("add_and_activate_connection() succeeded"), - Err(e) => { - eprintln!("add_and_activate_connection() failed: {e}"); - return Err(e); + if use_saved_connection { + let conn_path = saved_conn_path.unwrap(); + eprintln!("Activating saved connection: {}", conn_path.as_str()); + + match nm + .activate_connection( + conn_path.clone(), + wifi_device.clone(), + specific_object.clone(), + ) + .await + { + Ok(active_conn) => { + eprintln!( + "activate_connection() succeeded, active connection: {}", + active_conn.as_str() + ); + + Delay::new(Duration::from_millis(500)).await; + + let dev_check = NMDeviceProxy::builder(&self.conn) + .path(wifi_device.clone())? + .build() + .await?; + + let check_state = dev_check.state().await?; + + if check_state == 30 { + eprintln!("Connection activated but device stuck in Disconnected state"); + eprintln!("Saved connection has invalid settings, deleting and retrying"); + + let _ = nm.deactivate_connection(active_conn).await; + + let _ = self.delete_connection(conn_path).await; + + let opts = ConnectionOptions { + autoconnect: true, + autoconnect_priority: None, + autoconnect_retries: None, + }; + + let settings = build_wifi_connection(ssid, &creds, &opts); + + eprintln!("Creating fresh connection with corrected settings"); + match nm + .add_and_activate_connection( + settings, + wifi_device.clone(), + specific_object, + ) + .await + { + Ok(_) => eprintln!("Fresh connection created successfully"), + Err(e) => { + eprintln!("Fresh connection also failed: {e}"); + return Err(e); + } + } + } + } + Err(e) => { + eprintln!("activate_connection() failed: {e}"); + eprintln!( + "Saved connection may be corrupted, deleting and retrying with fresh connection" + ); + + let _ = self.delete_connection(conn_path).await; + + let opts = ConnectionOptions { + autoconnect: true, + autoconnect_priority: None, + autoconnect_retries: None, + }; + + let settings = build_wifi_connection(ssid, &creds, &opts); + + eprintln!("Creating fresh connection after saved connection failed"); + return match nm + .add_and_activate_connection(settings, wifi_device.clone(), specific_object) + .await + { + Ok(_) => { + eprintln!("Successfully created fresh connection"); + Ok(()) + } + Err(e) => { + eprintln!("Fresh connection also failed: {e}"); + Err(e) + } + }; + } + } + } else { + let opts = ConnectionOptions { + autoconnect: true, + autoconnect_priority: None, + autoconnect_retries: None, + }; + + let settings = build_wifi_connection(ssid, &creds, &opts); + + println!("Creating new connection, settings: \n{settings:#?}"); + match nm + .add_and_activate_connection(settings, wifi_device.clone(), specific_object) + .await + { + Ok(_) => eprintln!("add_and_activate_connection() succeeded"), + Err(e) => { + eprintln!("add_and_activate_connection() failed: {e}"); + return Err(e); + } } } + Delay::new(Duration::from_millis(300)).await; + let dev_proxy = NMDeviceProxy::builder(&self.conn) .path(wifi_device.clone())? .build() .await?; - eprintln!("Dev state after connect(): {:?}", dev_proxy.state().await?); + + let initial_state = dev_proxy.state().await?; + eprintln!("Dev state after connect(): {initial_state:?}"); + + // Wait to reach state 100 - This brings us to `Activated` + // States: 30=Disconnected, 40=Prepare, 50=Config, 60=IP Config, 70=IP Check, 100=Activated, 120=Failed + eprintln!("Waiting for connection to complete..."); + for i in 0..40 { + Delay::new(Duration::from_millis(500)).await; + match dev_proxy.state().await { + Ok(state) => { + let device_state = DeviceState::from(state); + eprintln!("Connection progress {i}: state = {device_state:?} ({state})"); + + if state == 100 { + eprintln!("✓ Connection activated successfully!"); + break; + } else if state == 120 { + eprintln!("✗ Connection failed!"); + + // Try to get the failure reason + if let Ok(reason) = dev_proxy.state_reason().await { + eprintln!("Failure reason code: {reason:?}"); + let reason_str = match reason.1 { + 0 => "Unknown", + 1 => "None", + 2 => "User disconnected", + 3 => "Device disconnected", + 4 => "Carrier changed", + 7 => "Supplicant disconnected", + 8 => "Supplicant config failed", + 9 => "Supplicant failed", + 10 => "Supplicant timeout", + 11 => "PPP start failed", + 15 => "DHCP start failed", + 16 => "DHCP error", + 17 => "DHCP failed", + 24 => "Modem connection failed", + 25 => "Modem init failed", + 42 => "Infiniband mode", + 43 => "Dependency failed", + 44 => "BR2684 failed", + 45 => "Mode set failed", + 46 => "GSM APN select failed", + 47 => "GSM not searching", + 48 => "GSM registration denied", + 49 => "GSM registration timeout", + 50 => "GSM registration failed", + 51 => "GSM PIN check failed", + 52 => "Firmware missing", + 53 => "Device removed", + 54 => "Sleeping", + 55 => "Connection removed", + 56 => "User requested", + 57 => "Carrier", + 58 => "Connection assumed", + 59 => "Supplicant available", + 60 => "Modem not found", + 61 => "Bluetooth failed", + 62 => "GSM SIM not inserted", + 63 => "GSM SIM PIN required", + 64 => "GSM SIM PUK required", + 65 => "GSM SIM wrong", + 66 => "InfiniBand mode", + 67 => "Dependency failed", + 68 => "BR2684 failed", + 69 => "Modem manager unavailable", + 70 => "SSID not found", + 71 => "Secondary connection failed", + 72 => "DCB or FCoE setup failed", + 73 => "Teamd control failed", + 74 => "Modem failed or no longer available", + 75 => "Modem now ready and available", + 76 => "SIM PIN was incorrect", + 77 => "New connection activation enqueued", + 78 => "Parent device unreachable", + 79 => "Parent device changed", + _ => "Unknown reason", + }; + eprintln!("Failure details: {reason_str}"); + } + + return Err(zbus::Error::Failure( + "Connection failed - authentication or network issue".into(), + )); + } else if i > 10 && state == 30 { + // If we're still disconnected after 5 seconds, something's wrong + eprintln!("✗ Connection stuck in disconnected state"); + return Err(zbus::Error::Failure( + "Connection failed - stuck in disconnected state".into(), + )); + } + } + Err(e) => { + eprintln!("Failed to check device state: {e}"); + break; + } + } + } + eprintln!("---Connection request for '{ssid}' submitted successfully---"); Ok(()) @@ -579,10 +812,102 @@ impl NetworkManager { } } + pub async fn has_saved_connection(&self, ssid: &str) -> zbus::Result { + self.get_saved_connection_path(ssid) + .await + .map(|p| p.is_some()) + } + + pub async fn get_saved_connection_path( + &self, + ssid: &str, + ) -> zbus::Result> { + use std::collections::HashMap; + use zvariant::{OwnedObjectPath, Value}; + + let settings = zbus::proxy::Proxy::new( + &self.conn, + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager/Settings", + "org.freedesktop.NetworkManager.Settings", + ) + .await?; + + let reply = settings.call_method("ListConnections", &()).await?; + let conns: Vec = reply.body().deserialize()?; + + for cpath in conns { + let cproxy = zbus::proxy::Proxy::new( + &self.conn, + "org.freedesktop.NetworkManager", + cpath.as_str(), + "org.freedesktop.NetworkManager.Settings.Connection", + ) + .await?; + + let msg = cproxy.call_method("GetSettings", &()).await?; + let body = msg.body(); + let all: HashMap> = body.deserialize()?; + + if let Some(conn_section) = all.get("connection") + && let Some(Value::Str(id)) = conn_section.get("id") + && id == ssid + { + return Ok(Some(cpath)); + } + } + + Ok(None) + } + + #[deprecated(note = "Use get_saved_connection_path instead")] + async fn _old_has_saved_connection(&self, ssid: &str) -> zbus::Result { + use std::collections::HashMap; + use zvariant::{OwnedObjectPath, Value}; + + let settings = zbus::proxy::Proxy::new( + &self.conn, + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager/Settings", + "org.freedesktop.NetworkManager.Settings", + ) + .await?; + + let reply = settings.call_method("ListConnections", &()).await?; + let conns: Vec = reply.body().deserialize()?; + + for cpath in conns { + let cproxy = zbus::proxy::Proxy::new( + &self.conn, + "org.freedesktop.NetworkManager", + cpath.as_str(), + "org.freedesktop.NetworkManager.Settings.Connection", + ) + .await?; + + let msg = cproxy.call_method("GetSettings", &()).await?; + let body = msg.body(); + let all: HashMap> = body.deserialize()?; + + if let Some(conn_section) = all.get("connection") + && let Some(Value::Str(id)) = conn_section.get("id") + && id == ssid + && let Some(Value::Bool(ac)) = conn_section.get("autoconnect") + { + eprintln!("autoconnect: {ac}"); + return Ok(true); + } + } + + Ok(false) + } + pub async fn forget(&self, ssid: &str) -> zbus::Result<()> { use std::collections::HashMap; use zvariant::{OwnedObjectPath, Value}; + eprintln!("Starting forget operation for: {ssid}"); + let nm = NMProxy::new(&self.conn).await?; let devices = nm.get_devices().await?; @@ -609,24 +934,43 @@ impl NetworkManager { if let Ok(bytes) = ap.ssid().await && std::str::from_utf8(&bytes).ok() == Some(ssid) { + eprintln!("Disconnecting from active network: {ssid}"); let dev_proxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(&self.conn) .destination("org.freedesktop.NetworkManager")? .path(dev_path.clone())? .interface("org.freedesktop.NetworkManager.Device")? .build() .await?; - let _ = dev_proxy.call_method("Disconnect", &()).await; - for _ in 0..10 { - if dev.state().await? == 30 { - break; + match dev_proxy.call_method("Disconnect", &()).await { + Ok(_) => eprintln!("Disconnect call succeeded"), + Err(e) => eprintln!("Disconnect call failed: {e}"), + } + + eprintln!("About to enter wait loop..."); + for i in 0..20 { + Delay::new(Duration::from_millis(200)).await; + match dev.state().await { + Ok(current_state) => { + eprintln!("Wait loop {i}: device state = {current_state}"); + if current_state == 30 || current_state == 20 { + eprintln!("Device reached disconnected state"); + break; + } + } + Err(e) => { + eprintln!("Failed to get device state in wait loop {i}: {e}"); + break; + } } - tokio::time::sleep(std::time::Duration::from_millis(200)).await; } + eprintln!("Wait loop completed"); } } } + eprintln!("Starting connection deletion phase..."); + let settings: zbus::Proxy<'_> = zbus::proxy::Builder::new(&self.conn) .destination("org.freedesktop.NetworkManager")? .path("/org/freedesktop/NetworkManager/Settings")? @@ -637,7 +981,7 @@ impl NetworkManager { let list_reply = settings.call_method("ListConnections", &()).await?; let conns: Vec = list_reply.body().deserialize()?; - let mut deleted_any = false; + let mut deleted_count = 0; for cpath in conns { let cproxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(&self.conn) @@ -647,25 +991,18 @@ impl NetworkManager { .build() .await?; - if let Ok(id) = cproxy.get_property::("Id").await - && id == ssid - { - let _ = cproxy.call_method("Delete", &()).await; - deleted_any = true; - continue; - } - if let Ok(msg) = cproxy.call_method("GetSettings", &()).await { let body = msg.body(); let settings_map: HashMap> = body.deserialize()?; + let mut should_delete = false; + if let Some(conn_sec) = settings_map.get("connection") && let Some(Value::Str(id)) = conn_sec.get("id") && id.as_str() == ssid { - let _ = cproxy.call_method("Delete", &()).await; - deleted_any = true; - continue; + should_delete = true; + eprintln!("Found connection by ID: {id}"); } if let Some(wifi_sec) = settings_map.get("802-11-wireless") @@ -678,21 +1015,55 @@ impl NetworkManager { } } if std::str::from_utf8(&raw).ok() == Some(ssid) { - let _ = cproxy.call_method("Delete", &()).await; - deleted_any = true; - continue; + should_delete = true; + eprintln!("Found connection by SSID match"); + } + } + + if let Some(wsec) = settings_map.get("802-11-wireless-security") { + let missing_psk = !wsec.contains_key("psk"); + let empty_psk = matches!(wsec.get("psk"), Some(Value::Str(s)) if s.is_empty()); + + if (missing_psk || empty_psk) && should_delete { + eprintln!("Connection has missing/empty PSK, will delete"); + } + } + + if should_delete { + match cproxy.call_method("Delete", &()).await { + Ok(_) => { + deleted_count += 1; + eprintln!("Deleted connection: {}", cpath.as_str()); + } + Err(e) => { + eprintln!("Failed to delete connection {}: {}", cpath.as_str(), e); + } } } } } - if deleted_any { - eprintln!("Forgot and disconnected '{ssid}'"); + if deleted_count > 0 { + eprintln!("Successfully deleted {deleted_count} connection(s) for '{ssid}'"); Ok(()) } else { + eprintln!("No saved connections found for '{ssid}'"); Err(zbus::Error::Failure(format!( "No saved connection for {ssid}" ))) } } + + async fn delete_connection(&self, conn_path: OwnedObjectPath) -> zbus::Result<()> { + let cproxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(&self.conn) + .destination("org.freedesktop.NetworkManager")? + .path(conn_path.clone())? + .interface("org.freedesktop.NetworkManager.Settings.Connection")? + .build() + .await?; + + cproxy.call_method("Delete", &()).await?; + eprintln!("Deleted connection: {}", conn_path.as_str()); + Ok(()) + } } diff --git a/nmrs-core/src/models.rs b/nmrs-core/src/models.rs index 0fa0dc7d..de009ea2 100644 --- a/nmrs-core/src/models.rs +++ b/nmrs-core/src/models.rs @@ -8,6 +8,7 @@ pub struct Network { pub ssid: String, pub bssid: Option, pub strength: Option, + pub frequency: Option, pub secured: bool, pub is_psk: bool, pub is_eap: bool, @@ -58,6 +59,12 @@ pub struct EapOptions { pub phase2: Phase2, } +pub struct ConnectionOptions { + pub autoconnect: bool, + pub autoconnect_priority: Option, + pub autoconnect_retries: Option, +} + pub enum WifiSecurity { Open, WpaPsk { psk: String }, diff --git a/nmrs-core/src/wifi_builders.rs b/nmrs-core/src/wifi_builders.rs index 29eb2360..add43b04 100644 --- a/nmrs-core/src/wifi_builders.rs +++ b/nmrs-core/src/wifi_builders.rs @@ -1,12 +1,18 @@ +use models::ConnectionOptions; use std::collections::HashMap; use zvariant::Value; -use crate::models; +use crate::models::{self, EapMethod}; -/*fn bytes(val: &str) -> Vec { +fn bytes(val: &str) -> Vec { val.as_bytes().to_vec() } +fn string_array(xs: &[&str]) -> Value<'static> { + let vals: Vec = xs.iter().map(|s| s.to_string()).collect(); + Value::from(vals) +} + fn base_wifi_section(ssid: &str) -> HashMap<&'static str, Value<'static>> { let mut s = HashMap::new(); s.insert("ssid", Value::from(bytes(ssid))); @@ -14,23 +20,39 @@ fn base_wifi_section(ssid: &str) -> HashMap<&'static str, Value<'static>> { s } -fn base_connection_section(ssid: &str) -> HashMap<&'static str, Value<'static>> { +fn base_connection_section( + ssid: &str, + opts: &ConnectionOptions, +) -> HashMap<&'static str, Value<'static>> { let mut s = HashMap::new(); s.insert("type", Value::from("802-11-wireless")); s.insert("id", Value::from(ssid.to_string())); s.insert("uuid", Value::from(uuid::Uuid::new_v4().to_string())); - s.insert("autoconnect", Value::from(true)); + s.insert("autoconnect", Value::from(opts.autoconnect)); + + if let Some(p) = opts.autoconnect_priority { + s.insert("autoconnect-priority", Value::from(p)); + } + + if let Some(r) = opts.autoconnect_retries { + s.insert("autoconnect-retries", Value::from(r)); + } + s } fn build_psk_security(psk: &str) -> HashMap<&'static str, Value<'static>> { let mut sec = HashMap::new(); + sec.insert("key-mgmt", Value::from("wpa-psk")); sec.insert("psk", Value::from(psk.to_string())); - // hardening maybe - // sec.insert("proto", Value::from(vec!["rsn"])); - // pairwise - // etc... + sec.insert("psk-flags", Value::from(0u32)); // 0 = agent-owned, provided during activation + sec.insert("auth-alg", Value::from("open")); + + sec.insert("proto", string_array(&["rsn"])); + sec.insert("pairwise", string_array(&["ccmp"])); + sec.insert("group", string_array(&["ccmp"])); + sec } @@ -42,17 +64,18 @@ fn build_eap_security( ) { let mut sec = HashMap::new(); sec.insert("key-mgmt", Value::from("wpa-eap")); - sec.insert("auth-alg", Value::from("OPEN")); + sec.insert("auth-alg", Value::from("open")); // same hardening tips as psk // proto, pairwise, group, etc. // 802-1x let mut e1x = HashMap::new(); - let eap_vec = match opts.method { - models::EapMethod::Peap => vec!["peap"], - models::EapMethod::Ttls => vec!["ttls"], + + let eap_str = match opts.method { + EapMethod::Peap => "peap", + EapMethod::Ttls => "ttls", }; - e1x.insert("eap", Value::from(eap_vec)); + e1x.insert("eap", string_array(&[eap_str])); e1x.insert("identity", Value::from(opts.identity.clone())); e1x.insert("password", Value::from(opts.password.clone())); @@ -86,67 +109,48 @@ fn build_eap_security( pub fn build_wifi_connection( ssid: &str, security: &models::WifiSecurity, + opts: &ConnectionOptions, ) -> HashMap<&'static str, HashMap<&'static str, Value<'static>>> { let mut conn: HashMap<&'static str, HashMap<&'static str, Value<'static>>> = HashMap::new(); - conn.insert("connection", base_connection_section(ssid)); + + // base connections + conn.insert("connection", base_connection_section(ssid, opts)); conn.insert("802-11-wireless", base_wifi_section(ssid)); + // Add IPv4 and IPv6 configuration to prevent state 60 stall + // TODO: Expand upon auto/manual configuration options + let mut ipv4 = HashMap::new(); + ipv4.insert("method", Value::from("auto")); + conn.insert("ipv4", ipv4); + + let mut ipv6 = HashMap::new(); + ipv6.insert("method", Value::from("auto")); + conn.insert("ipv6", ipv6); + match security { models::WifiSecurity::Open => {} models::WifiSecurity::WpaPsk { psk } => { - conn.insert("802-11-wireless-security", build_psk_security(psk.as_str())); - } + // point wireless at security section + if let Some(w) = conn.get_mut("802-11-wireless") { + w.insert("security", Value::from("802-11-wireless-security")); + } - models::WifiSecurity::WpaEap { opts } => { - let (sec, e1x) = build_eap_security(&opts); + let sec = build_psk_security(psk); conn.insert("802-11-wireless-security", sec); - conn.insert("802-1x", e1x); } - } - conn -}*/ - -pub fn build_wifi_connection( - ssid: &str, - security: &models::WifiSecurity, -) -> HashMap<&'static str, HashMap<&'static str, zvariant::Value<'static>>> { - let mut conn = HashMap::new(); - - let mut s_conn = HashMap::new(); - s_conn.insert("type", Value::from("802-11-wireless")); - s_conn.insert("id", Value::from(ssid.to_string())); - s_conn.insert("uuid", Value::from(uuid::Uuid::new_v4().to_string())); - s_conn.insert("autoconnect", Value::from(true)); - s_conn.insert("interface-name", Value::from("wlan0")); - conn.insert("connection", s_conn); - let mut s_wifi = HashMap::new(); - s_wifi.insert("ssid", Value::from(ssid.as_bytes().to_vec())); - s_wifi.insert("mode", Value::from("infrastructure")); + models::WifiSecurity::WpaEap { opts } => { + if let Some(w) = conn.get_mut("802-11-wireless") { + w.insert("security", Value::from("802-11-wireless-security")); + } - match security { - models::WifiSecurity::Open => {} - models::WifiSecurity::WpaPsk { psk } => { - s_wifi.insert("security", Value::from("802-11-wireless-security")); - let mut s_sec = HashMap::new(); - s_sec.insert("key-mgmt", Value::from("wpa-psk")); - s_sec.insert("auth-alg", Value::from("open")); - s_sec.insert("psk", Value::from(psk.to_string())); - conn.insert("802-11-wireless-security", s_sec); + let (mut sec, e1x) = build_eap_security(opts); + sec.insert("auth-alg", Value::from("open")); + conn.insert("802-11-wireless-security", sec); + conn.insert("802-1x", e1x); } - _ => {} } - conn.insert("802-11-wireless", s_wifi); - - let mut ipv4 = HashMap::new(); - ipv4.insert("method", Value::from("auto")); - conn.insert("ipv4", ipv4); - - let mut ipv6 = HashMap::new(); - ipv6.insert("method", Value::from("auto")); - conn.insert("ipv6", ipv6); - conn } diff --git a/nmrs-core/tests/wifi_buillders_test.rs b/nmrs-core/tests/wifi_buillders_test.rs index 7c455192..7b9c7c4f 100644 --- a/nmrs-core/tests/wifi_buillders_test.rs +++ b/nmrs-core/tests/wifi_buillders_test.rs @@ -1,10 +1,18 @@ -use nmrs_core::models::WifiSecurity; +use nmrs_core::models::{ConnectionOptions, WifiSecurity}; use nmrs_core::wifi_builders::build_wifi_connection; use zvariant::Value; +fn opts() -> ConnectionOptions { + ConnectionOptions { + autoconnect: true, + autoconnect_priority: None, + autoconnect_retries: None, + } +} + #[test] fn builds_open_wifi_connection() { - let conn = build_wifi_connection("testnet", &WifiSecurity::Open); + let conn = build_wifi_connection("testnet", &WifiSecurity::Open, &opts()); assert!(conn.contains_key("connection")); assert!(conn.contains_key("802-11-wireless")); assert!(conn.contains_key("ipv4")); @@ -18,6 +26,7 @@ fn builds_psk_wifi_connection_with_security_section() { &WifiSecurity::WpaPsk { psk: "pw123".into(), }, + &opts(), ); let has_sec = conn.contains_key("802-11-wireless-security"); assert!(has_sec, "security section missing"); diff --git a/nmrs-ui/src/ui/connect.rs b/nmrs-ui/src/ui/connect.rs index afcfaacb..c7b39e0b 100644 --- a/nmrs-ui/src/ui/connect.rs +++ b/nmrs-ui/src/ui/connect.rs @@ -68,17 +68,29 @@ fn draw_connect_modal(parent: &ApplicationWindow, ssid: &str, is_eap: bool) { let ssid_owned = ssid.to_string(); let user_entry_clone = user_entry.clone(); + let status_label = Label::new(Some("")); + status_label.add_css_class("status-label"); + vbox.append(&status_label); + { let dialog_rc = dialog_rc.clone(); + let status_label = status_label.clone(); + entry.connect_activate(move |entry| { let pwd = entry.text().to_string(); + let username = user_entry_clone .as_ref() .map(|e| e.text().to_string()) .unwrap_or_default(); let ssid = ssid_owned.clone(); + let dialog = dialog_rc.clone(); + let status = status_label.clone(); + let entry_clone = entry.clone(); - eprintln!("User entered username={username}, password={pwd}"); + // Prevent double submission + entry.set_sensitive(false); + status.set_text("Connecting..."); glib::MainContext::default().spawn_local(async move { eprintln!("---in spawned task here--"); @@ -107,17 +119,28 @@ fn draw_connect_modal(parent: &ApplicationWindow, ssid: &str, is_eap: bool) { println!("Calling nm.connect() for '{ssid}'"); match nm.connect(&ssid, creds).await { - Ok(_) => println!("nm.connect() succeeded!"), - Err(err) => eprintln!("nm.connect() failed: {err}"), + Ok(_) => { + println!("nm.connect() succeeded!"); + status.set_text("✓ Connected!"); + glib::timeout_future_seconds(1).await; + dialog.close(); + } + Err(err) => { + eprintln!("nm.connect() failed: {err}"); + status.set_text(&format!("✗ Failed: {err}")); + entry_clone.set_sensitive(true); + } } } - Err(err) => eprintln!("Failed to create NetworkManager: {err}"), + Err(err) => { + eprintln!("Failed to create NetworkManager: {err}"); + status.set_text(&format!("✗ Error: {err}")); + entry_clone.set_sensitive(true); + } } - println!("---finsihed spawned task---"); + println!("---finished spawned task---"); }); - - dialog_rc.close(); }); } diff --git a/nmrs-ui/src/ui/header.rs b/nmrs-ui/src/ui/header.rs index fb0cce28..999ebe1b 100644 --- a/nmrs-ui/src/ui/header.rs +++ b/nmrs-ui/src/ui/header.rs @@ -1,15 +1,6 @@ -use futures_util::StreamExt; -use glib::ControlFlow; use gtk::prelude::*; use gtk::{Box as GtkBox, HeaderBar, Label, ListBox, Orientation, Switch}; use nmrs_core::NetworkManager; -use nmrs_core::dbus::{NMProxy, NMWirelessProxy}; -use std::cell::Cell; -use zbus::Connection; - -thread_local! { - static REFRESH_SCHEDULED: Cell = const { Cell::new(false) }; -} use crate::ui::networks; @@ -60,9 +51,10 @@ pub fn build_header( if enabled { status_clone.set_text("Scanning..."); let _ = nm.scan_networks().await; - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + glib::timeout_future_seconds(2).await; match nm.list_networks().await { Ok(nets) => { + status_clone.set_text(""); let list: ListBox = networks::networks_view(&nets, &pw, &stack_clone); list_container_clone.append(&list); @@ -107,14 +99,7 @@ pub fn build_header( if nm.wait_for_wifi_ready().await.is_ok() { let _ = nm.scan_networks().await; status_clone.set_text("Scanning..."); - - spawn_signal_listeners( - list_container_clone.clone(), - status_clone.clone(), - &pw, - &stack_inner, - ) - .await; + glib::timeout_future_seconds(2).await; match nm.list_networks().await { Ok(nets) => { @@ -150,131 +135,3 @@ fn clear_children(container: >k::Box) { container.remove(&widget); } } - -async fn spawn_signal_listeners( - list_container: GtkBox, - status: Label, - parent_window: >k::ApplicationWindow, - stack: >k::Stack, -) { - let conn = match Connection::system().await { - Ok(c) => c, - Err(e) => { - status.set_text(&format!("DBus conn error: {e}")); - return; - } - }; - - let nm_proxy = match NMProxy::new(&conn).await { - Ok(p) => p, - Err(e) => { - status.set_text(&format!("NM proxy error: {e}")); - return; - } - }; - - let devices = match nm_proxy.get_devices().await { - Ok(d) => d, - Err(e) => { - status.set_text(&format!("Get devices error: {e}")); - return; - } - }; - - for dev_path in devices { - let builder = match NMWirelessProxy::builder(&conn).path(dev_path.clone()) { - Ok(b) => b, - Err(_) => continue, - }; - - let wifi = match builder.build().await { - Ok(proxy) => proxy, - Err(_) => continue, - }; - - let mut added = match wifi.receive_access_point_added().await { - Ok(s) => s, - Err(_) => continue, - }; - let mut removed = match wifi.receive_access_point_removed().await { - Ok(s) => s, - Err(_) => continue, - }; - - let list_container_signal = list_container.clone(); - let status_signal = status.clone(); - let pw = parent_window.clone(); - let stack_signal = stack.clone(); - - glib::MainContext::default().spawn_local(async move { - loop { - tokio::select! { - event = added.next() => match event { - Some(_) => { - schedule_refresh(list_container_signal.clone(), status_signal.clone(), &pw, &stack_signal).await; - } - None => break, - }, - event = removed.next() => match event { - Some(_) => { - schedule_refresh(list_container_signal.clone(), status_signal.clone(), &pw, &stack_signal).await; - } - None => break, - }, - } - } - }); - } -} - -async fn refresh_networks( - list_container: &GtkBox, - status: &Label, - parent_window: >k::ApplicationWindow, - stack: >k::Stack, -) { - match NetworkManager::new().await { - Ok(nm) => match nm.list_networks().await { - Ok(nets) => { - clear_children(list_container); - let list: ListBox = networks::networks_view(&nets, parent_window, stack); - list_container.append(&list); - } - Err(e) => status.set_text(&format!("Error refreshing networks: {e}")), - }, - Err(e) => status.set_text(&format!("Error refreshing networks: {e}")), - } -} - -async fn schedule_refresh( - list_container: GtkBox, - status: Label, - parent_window: >k::ApplicationWindow, - stack: >k::Stack, -) { - REFRESH_SCHEDULED.with(|flag| { - if flag.get() { - return; - } - flag.set(true); - - let list_container_clone = list_container.clone(); - let status_clone = status.clone(); - let pw = parent_window.clone(); - let stack_clone = stack.clone(); - - glib::timeout_add_seconds_local(1, move || { - let list_container_inner = list_container_clone.clone(); - let status_inner = status_clone.clone(); - let pw2 = pw.clone(); - let stack_inner = stack_clone.clone(); - - glib::MainContext::default().spawn_local(async move { - refresh_networks(&list_container_inner, &status_inner, &pw2, &stack_inner).await; - REFRESH_SCHEDULED.with(|f| f.set(false)); - }); - - ControlFlow::Break - }); - }); -} diff --git a/nmrs-ui/src/ui/network_page.rs b/nmrs-ui/src/ui/network_page.rs index f235510b..102cd514 100644 --- a/nmrs-ui/src/ui/network_page.rs +++ b/nmrs-ui/src/ui/network_page.rs @@ -38,21 +38,29 @@ pub fn network_page(info: NetworkInfo, stack: >k::Stack) -> Box { forget_btn.set_valign(Align::Center); forget_btn.set_cursor_from_name(Some("pointer")); - forget_btn.connect_clicked(clone!( - #[strong] - info, - #[weak] - stack, - move |_| { - let ssid = info.ssid.clone(); + { + let info_clone = info.clone(); + let stack_clone = stack.clone(); + + forget_btn.connect_clicked(move |_| { + let ssid = info_clone.ssid.clone(); + let stack = stack_clone.clone(); + glib::MainContext::default().spawn_local(async move { if let Ok(nm) = NetworkManager::new().await { - let _ = nm.forget(&ssid).await; - stack.set_visible_child_name("networks"); + match nm.forget(&ssid).await { + Ok(_) => { + eprintln!("Successfully forgot network: {ssid}"); + stack.set_visible_child_name("networks"); + } + Err(e) => { + eprintln!("Failed to forget network {ssid}: {e}"); + } + } } }); - } - )); + }); + } header.append(&icon); header.append(&title); diff --git a/nmrs-ui/src/ui/networks.rs b/nmrs-ui/src/ui/networks.rs index 6dafc822..03f67201 100644 --- a/nmrs-ui/src/ui/networks.rs +++ b/nmrs-ui/src/ui/networks.rs @@ -17,13 +17,34 @@ pub fn networks_view( let conn_threshold = 75; let list = ListBox::new(); - for net in networks { + // Sort networks by signal strength (descending) + let mut sorted_networks = networks.to_vec(); + sorted_networks.sort_by(|a, b| b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0))); + + for net in sorted_networks { let row = ListBoxRow::new(); let hbox = Box::new(Orientation::Horizontal, 6); let gesture = GestureClick::new(); row.add_css_class("network-selection"); - let ssid = Label::new(Some(&net.ssid)); + + // Add band suffix for display only + let display_name = if let Some(freq) = net.frequency { + let band = if (2400..=2500).contains(&freq) { + " (2.4GHz)" + } else if (5000..=6000).contains(&freq) { + " (5GHz)" + } else if (5925..=7125).contains(&freq) { + " (6GHz)" + } else { + "" + }; + format!("{}{}", net.ssid, band) + } else { + net.ssid.clone() + }; + + let ssid = Label::new(Some(&display_name)); hbox.append(&ssid); let spacer = Box::new(Orientation::Horizontal, 0); @@ -97,21 +118,37 @@ pub fn networks_view( #[weak] parent_window, move |_, n_press, _x, _y| { - if n_press == 2 && secured { - connect::connect_modal(&parent_window, &ssid_str, is_eap); - } else if n_press == 2 { - glib::MainContext::default().spawn_local({ - let ssid = ssid_str.clone(); - async move { - match NetworkManager::new().await { - Ok(nm) => { + if n_press == 2 { + let ssid2 = ssid_str.clone(); + let window = parent_window.clone(); + + glib::MainContext::default().spawn_local(async move { + match NetworkManager::new().await { + Ok(nm) => { + if secured { + let have = + nm.has_saved_connection(&ssid2).await.unwrap_or(false); + + if have { + let creds = WifiSecurity::WpaPsk { + psk: "".into(), // TODO: NM will use saved secrets + }; + let _ = nm.connect(&ssid2, creds).await; + } else { + connect::connect_modal(&window, &ssid2, is_eap); + } + } else { + eprintln!("Connecting to open network: {ssid2}"); let creds = WifiSecurity::Open; - if let Err(err) = nm.connect(&ssid, creds).await { - eprintln!("Failed to connect network: {err}"); + match nm.connect(&ssid2, creds).await { + Ok(_) => eprintln!("Successfully connected to {ssid2}"), + Err(e) => { + eprintln!("Failed to connect to {ssid2}: {e}") + } } } - Err(err) => eprintln!("Failed to init NetworkManager: {err}"), } + Err(e) => eprintln!("nm init fail: {e}"), } }); }