use futures_timer::Delay;
use log::{debug, error, info, warn};
use std::collections::HashMap;
use zbus::Connection;
use zvariant::OwnedObjectPath;
use crate::Result;
use crate::api::builders::wifi::{build_ethernet_connection, build_wifi_connection};
use crate::api::models::{ConnectionError, ConnectionOptions, TimeoutConfig, WifiSecurity};
use crate::core::connection_settings::{delete_connection, get_saved_connection_path};
use crate::core::state_wait::{wait_for_connection_activation, wait_for_device_disconnect};
use crate::dbus::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWiredProxy, NMWirelessProxy};
use crate::monitoring::info::current_ssid;
use crate::monitoring::transport::ActiveTransport;
use crate::monitoring::wifi::Wifi;
use crate::types::constants::{device_state, device_type, timeouts};
use crate::util::utils::{decode_ssid_or_empty, nm_proxy};
use crate::util::validation::{validate_ssid, validate_wifi_security};
enum SavedDecision {
UseSaved(OwnedObjectPath),
RebuildFresh,
}
pub(crate) async fn connect(
conn: &Connection,
ssid: &str,
creds: WifiSecurity,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
validate_ssid(ssid)?;
validate_wifi_security(&creds)?;
debug!(
"Connecting to '{}' | secured={} is_psk={} is_eap={}",
ssid,
creds.secured(),
creds.is_psk(),
creds.is_eap()
);
let nm = NMProxy::new(conn).await?;
let saved_raw = get_saved_connection_path(conn, ssid).await?;
let decision = decide_saved_connection(saved_raw, &creds)?;
let wifi_device = find_wifi_device(conn, &nm).await?;
debug!("Found WiFi device: {}", wifi_device.as_str());
let wifi = NMWirelessProxy::builder(conn)
.path(wifi_device.clone())?
.build()
.await?;
if let Some(active) = Wifi::current(conn).await {
debug!("Currently connected to: {active}");
if active == ssid {
debug!("Already connected to {active}, skipping connect()");
return Ok(());
}
} else {
debug!("Not currently connected to any network");
}
let specific_object = scan_and_resolve_ap(conn, &wifi, ssid).await?;
match decision {
SavedDecision::UseSaved(saved) => {
ensure_disconnected(conn, &nm, &wifi_device, timeout_config).await?;
connect_via_saved(
conn,
&nm,
&wifi_device,
&specific_object,
&creds,
saved,
timeout_config,
)
.await?;
}
SavedDecision::RebuildFresh => {
build_and_activate_new(
conn,
&nm,
&wifi_device,
&specific_object,
ssid,
creds,
timeout_config,
)
.await?;
}
}
info!("Successfully connected to '{ssid}'");
Ok(())
}
pub(crate) async fn connect_wired(
conn: &Connection,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
debug!("Connecting to wired device");
let nm = NMProxy::new(conn).await?;
let wired_device = find_wired_device(conn, &nm).await?;
debug!("Found wired device: {}", wired_device.as_str());
let dev = NMDeviceProxy::builder(conn)
.path(wired_device.clone())?
.build()
.await?;
let current_state = dev.state().await?;
if current_state == device_state::ACTIVATED {
debug!("Wired device already activated, skipping connect()");
return Ok(());
}
let interface = dev.interface().await?;
let saved = get_saved_connection_path(conn, &interface).await?;
let specific_object = OwnedObjectPath::default();
match saved {
Some(saved_path) => {
debug!("Activating saved wired connection: {}", saved_path.as_str());
let active_conn = nm
.activate_connection(saved_path, wired_device.clone(), specific_object)
.await?;
let timeout = timeout_config.map(|c| c.connection_timeout);
wait_for_connection_activation(conn, &active_conn, timeout).await?;
}
None => {
debug!("No saved connection found, creating new wired connection");
let opts = ConnectionOptions {
autoconnect: true,
autoconnect_priority: None,
autoconnect_retries: None,
};
let settings = build_ethernet_connection(&interface, &opts);
let (_, active_conn) = nm
.add_and_activate_connection(settings, wired_device.clone(), specific_object)
.await?;
let timeout = timeout_config.map(|c| c.connection_timeout);
wait_for_connection_activation(conn, &active_conn, timeout).await?;
}
}
if let Ok(wired) = NMWiredProxy::builder(conn)
.path(wired_device.clone())?
.build()
.await
&& let Ok(speed) = wired.speed().await
{
info!("Connected to wired device at {speed} Mb/s");
}
info!("Successfully connected to wired device");
Ok(())
}
pub(crate) async fn forget_by_name_and_type(
conn: &Connection,
name: &str,
device_filter: Option<u32>,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
use std::collections::HashMap;
use zvariant::{OwnedObjectPath, Value};
validate_ssid(name)?;
debug!(
"Starting forget operation for: {name} (device filter: {:?})",
device_filter
);
let nm = NMProxy::new(conn).await?;
let devices = nm.get_devices().await?;
for dev_path in &devices {
let dev = NMDeviceProxy::builder(conn)
.path(dev_path.clone())?
.build()
.await?;
let dev_type = dev.device_type().await?;
if let Some(filter) = device_filter
&& dev_type != filter
{
continue;
}
if dev_type == device_type::WIFI {
let wifi = NMWirelessProxy::builder(conn)
.path(dev_path.clone())?
.build()
.await?;
if let Ok(ap_path) = wifi.active_access_point().await
&& ap_path.as_str() != "/"
{
let ap = NMAccessPointProxy::builder(conn)
.path(ap_path.clone())?
.build()
.await?;
if let Ok(bytes) = ap.ssid().await
&& decode_ssid_or_empty(&bytes) == name
{
debug!("Disconnecting from active WiFi network: {name}");
if let Err(e) = disconnect_wifi_and_wait(conn, dev_path, timeout_config).await {
warn!("Disconnect wait failed: {e}");
let final_state = dev.state().await?;
if final_state != device_state::DISCONNECTED
&& final_state != device_state::UNAVAILABLE
{
error!(
"Device still connected (state: {final_state}), cannot safely delete"
);
return Err(ConnectionError::Stuck(format!(
"disconnect failed, device in state {final_state}"
)));
}
debug!("Device confirmed disconnected, proceeding with deletion");
}
debug!("WiFi disconnect phase completed");
}
}
}
else if dev_type == device_type::BLUETOOTH {
let state = dev.state().await?;
if state != device_state::DISCONNECTED && state != device_state::UNAVAILABLE {
debug!("Disconnecting from active Bluetooth device: {name}");
if let Err(e) = crate::core::bluetooth::disconnect_bluetooth_and_wait(
conn,
dev_path,
timeout_config,
)
.await
{
warn!("Bluetooth disconnect failed: {e}");
let final_state = dev.state().await?;
if final_state != device_state::DISCONNECTED
&& final_state != device_state::UNAVAILABLE
{
error!(
"Bluetooth device still connected (state: {final_state}), cannot safely delete"
);
return Err(ConnectionError::Stuck(format!(
"disconnect failed, device in state {final_state}"
)));
}
}
debug!("Bluetooth disconnect phase completed");
}
}
}
debug!("Starting connection deletion phase...");
let settings = nm_proxy(
conn,
"/org/freedesktop/NetworkManager/Settings",
"org.freedesktop.NetworkManager.Settings",
)
.await?;
let list_reply = settings.call_method("ListConnections", &()).await?;
let conns: Vec<OwnedObjectPath> = list_reply.body().deserialize()?;
let mut deleted_count = 0;
for cpath in conns {
let cproxy = nm_proxy(
conn,
cpath.clone(),
"org.freedesktop.NetworkManager.Settings.Connection",
)
.await?;
if let Ok(msg) = cproxy.call_method("GetSettings", &()).await {
let body = msg.body();
let settings_map: HashMap<String, HashMap<String, Value>> = 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() == name
{
should_delete = true;
debug!("Found connection by ID: {id}");
}
if let Some(wifi_sec) = settings_map.get("802-11-wireless")
&& let Some(Value::Array(arr)) = wifi_sec.get("ssid")
{
let mut raw = Vec::new();
for v in arr.iter() {
if let Ok(b) = u8::try_from(v.clone()) {
raw.push(b);
}
}
if decode_ssid_or_empty(&raw) == name {
should_delete = true;
debug!("Found WiFi connection by SSID match");
}
}
if let Some(bt_sec) = settings_map.get("bluetooth")
&& let Some(Value::Str(bdaddr)) = bt_sec.get("bdaddr")
&& bdaddr.as_str() == name
{
should_delete = true;
debug!("Found Bluetooth connection by bdaddr 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 {
debug!("Connection has missing/empty PSK, will delete");
}
}
if should_delete {
match cproxy.call_method("Delete", &()).await {
Ok(_) => {
deleted_count += 1;
debug!("Deleted connection: {}", cpath.as_str());
}
Err(e) => {
warn!("Failed to delete connection {}: {}", cpath.as_str(), e);
}
}
}
}
}
if deleted_count > 0 {
info!("Successfully deleted {deleted_count} connection(s) for '{name}'");
Ok(())
} else {
debug!("No saved connections found for '{name}'");
if device_filter == Some(device_type::BLUETOOTH) {
debug!(
"Bluetooth device '{name}' has no NetworkManager connection profile (device may only be paired in BlueZ)"
);
Ok(())
} else {
Ok(())
}
}
}
pub(crate) async fn disconnect_wifi_and_wait(
conn: &Connection,
dev_path: &OwnedObjectPath,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
let dev = NMDeviceProxy::builder(conn)
.path(dev_path.clone())?
.build()
.await?;
let current_state = dev.state().await?;
if current_state == device_state::DISCONNECTED || current_state == device_state::UNAVAILABLE {
debug!("Device already disconnected");
return Ok(());
}
let raw = nm_proxy(
conn,
dev_path.clone(),
"org.freedesktop.NetworkManager.Device",
)
.await?;
debug!("Sending disconnect request");
match raw.call_method("Disconnect", &()).await {
Ok(_) => debug!("Disconnect method called successfully"),
Err(e) => warn!(
"Disconnect method call failed (device may already be disconnected): {}",
e
),
}
let timeout = timeout_config.map(|c| c.disconnect_timeout);
wait_for_device_disconnect(&dev, timeout).await?;
Delay::new(timeouts::stabilization_delay()).await;
Ok(())
}
async fn find_device_by_type(
conn: &Connection,
nm: &NMProxy<'_>,
device_type_id: u32,
) -> Result<OwnedObjectPath> {
let devices = nm.get_devices().await?;
for dp in devices {
let dev = NMDeviceProxy::builder(conn)
.path(dp.clone())?
.build()
.await?;
if dev.device_type().await? == device_type_id {
return Ok(dp);
}
}
match device_type_id {
device_type::WIFI => Err(ConnectionError::NoWifiDevice),
device_type::ETHERNET => Err(ConnectionError::NoWiredDevice),
_ => Err(ConnectionError::NoWifiDevice),
}
}
pub(crate) async fn find_wired_device(
conn: &Connection,
nm: &NMProxy<'_>,
) -> Result<OwnedObjectPath> {
find_device_by_type(conn, nm, device_type::ETHERNET).await
}
async fn find_wifi_device(conn: &Connection, nm: &NMProxy<'_>) -> Result<OwnedObjectPath> {
find_device_by_type(conn, nm, device_type::WIFI).await
}
async fn find_ap(
conn: &Connection,
wifi: &NMWirelessProxy<'_>,
target_ssid: &str,
) -> Result<OwnedObjectPath> {
let access_points = wifi.access_points().await?;
for ap_path in access_points {
let ap = NMAccessPointProxy::builder(conn)
.path(ap_path.clone())?
.build()
.await?;
let ssid_bytes = ap.ssid().await?;
let ssid = decode_ssid_or_empty(&ssid_bytes);
if ssid == target_ssid {
return Ok(ap_path);
}
}
Err(ConnectionError::NotFound)
}
async fn ensure_disconnected(
conn: &Connection,
nm: &NMProxy<'_>,
wifi_device: &OwnedObjectPath,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
if let Some(active) = Wifi::current(conn).await {
debug!("Disconnecting from {active}");
if let Ok(conns) = nm.active_connections().await {
for conn_path in conns {
match nm.deactivate_connection(conn_path.clone()).await {
Ok(_) => debug!("Connection deactivated during cleanup"),
Err(e) => warn!("Failed to deactivate connection during cleanup: {}", e),
}
}
}
disconnect_wifi_and_wait(conn, wifi_device, timeout_config).await?;
}
Ok(())
}
async fn connect_via_saved(
conn: &Connection,
nm: &NMProxy<'_>,
wifi_device: &OwnedObjectPath,
ap: &OwnedObjectPath,
creds: &WifiSecurity,
saved: OwnedObjectPath,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
debug!("Activating saved connection: {}", saved.as_str());
match nm
.activate_connection(saved.clone(), wifi_device.clone(), ap.clone())
.await
{
Ok(active_conn) => {
debug!(
"activate_connection() succeeded, active connection: {}",
active_conn.as_str()
);
let timeout = timeout_config.map(|c| c.connection_timeout);
match wait_for_connection_activation(conn, &active_conn, timeout).await {
Ok(()) => {
debug!("Saved connection activated successfully");
}
Err(e) => {
warn!("Saved connection activation failed: {e}");
warn!("Deleting saved connection and retrying with fresh credentials");
match nm.deactivate_connection(active_conn.clone()).await {
Ok(_) => debug!("Connection deactivated during cleanup"),
Err(e) => warn!("Failed to deactivate connection during cleanup: {}", e),
}
match delete_connection(conn, saved.clone()).await {
Ok(_) => debug!("Saved connection deleted"),
Err(e) => warn!("Failed to delete saved connection during recovery: {}", e),
}
let opts = ConnectionOptions {
autoconnect: true,
autoconnect_priority: None,
autoconnect_retries: None,
};
let settings = build_wifi_connection(ap.as_str(), creds, &opts);
debug!("Creating fresh connection with corrected settings");
let (_, new_active_conn) = nm
.add_and_activate_connection(settings, wifi_device.clone(), ap.clone())
.await
.map_err(|e| {
error!("Fresh connection also failed: {e}");
e
})?;
let timeout = timeout_config.map(|c| c.connection_timeout);
wait_for_connection_activation(conn, &new_active_conn, timeout).await?;
}
}
}
Err(e) => {
warn!("activate_connection() failed: {e}");
warn!("Saved connection may be corrupted, deleting and retrying with fresh connection");
match delete_connection(conn, saved.clone()).await {
Ok(_) => debug!("Saved connection deleted"),
Err(e) => warn!("Failed to delete saved connection during recovery: {}", e),
}
let opts = ConnectionOptions {
autoconnect: true,
autoconnect_priority: None,
autoconnect_retries: None,
};
let settings = build_wifi_connection(ap.as_str(), creds, &opts);
let (_, active_conn) = nm
.add_and_activate_connection(settings, wifi_device.clone(), ap.clone())
.await
.map_err(|e| {
error!("Fresh connection also failed: {e}");
e
})?;
let timeout = timeout_config.map(|c| c.connection_timeout);
wait_for_connection_activation(conn, &active_conn, timeout).await?;
}
}
Ok(())
}
async fn build_and_activate_new(
conn: &Connection,
nm: &NMProxy<'_>,
wifi_device: &OwnedObjectPath,
ap: &OwnedObjectPath,
ssid: &str,
creds: WifiSecurity,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
let opts = ConnectionOptions {
autoconnect: true,
autoconnect_retries: None,
autoconnect_priority: None,
};
let settings = build_wifi_connection(ssid, &creds, &opts);
debug!("Creating new connection, settings: \n{settings:#?}");
ensure_disconnected(conn, nm, wifi_device, timeout_config).await?;
let (_, active_conn) = match nm
.add_and_activate_connection(settings, wifi_device.clone(), ap.clone())
.await
{
Ok(paths) => {
debug!(
"add_and_activate_connection() succeeded, active connection: {}",
paths.1.as_str()
);
paths
}
Err(e) => {
error!("add_and_activate_connection() failed: {e}");
return Err(e.into());
}
};
debug!("Waiting for connection activation using signal monitoring...");
let timeout = timeout_config.map(|c| c.connection_timeout);
wait_for_connection_activation(conn, &active_conn, timeout).await?;
info!("Connection to '{ssid}' activated successfully");
Ok(())
}
async fn scan_and_resolve_ap(
conn: &Connection,
wifi: &NMWirelessProxy<'_>,
ssid: &str,
) -> Result<OwnedObjectPath> {
match wifi.request_scan(HashMap::new()).await {
Ok(_) => debug!("Scan requested successfully"),
Err(e) => warn!("Scan request failed: {e}"),
}
Delay::new(timeouts::scan_wait()).await;
debug!("Scan wait complete");
let ap = find_ap(conn, wifi, ssid).await?;
debug!("Matched target SSID '{ssid}'");
Ok(ap)
}
fn decide_saved_connection(
saved: Option<OwnedObjectPath>,
creds: &WifiSecurity,
) -> Result<SavedDecision> {
match saved {
Some(_) if matches!(creds, WifiSecurity::WpaPsk { psk } if !psk.trim().is_empty()) => {
Ok(SavedDecision::RebuildFresh)
}
Some(path) => Ok(SavedDecision::UseSaved(path)),
None if matches!(creds, WifiSecurity::WpaPsk { psk } if psk.trim().is_empty()) => {
Err(ConnectionError::MissingPassword)
}
None => Ok(SavedDecision::RebuildFresh),
}
}
pub(crate) async fn is_connected(conn: &Connection, ssid: &str) -> Result<bool> {
if let Some(active) = current_ssid(conn).await {
debug!("Currently connected to: {active}");
if active == ssid {
debug!("Already connected to {active}");
return Ok(true);
}
} else {
debug!("Not currently connected to any network");
}
Ok(false)
}
pub(crate) async fn disconnect(
conn: &Connection,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
let nm = NMProxy::new(conn).await?;
let wifi_device = match find_wifi_device(conn, &nm).await {
Ok(dev) => dev,
Err(ConnectionError::NoWifiDevice) => {
debug!("No WiFi device found");
return Ok(());
}
Err(e) => return Err(e),
};
let dev = NMDeviceProxy::builder(conn)
.path(wifi_device.clone())?
.build()
.await?;
let current_state = dev.state().await?;
if current_state == device_state::DISCONNECTED || current_state == device_state::UNAVAILABLE {
debug!("Device already disconnected");
return Ok(());
}
if let Ok(conns) = nm.active_connections().await {
for conn_path in conns {
match nm.deactivate_connection(conn_path.clone()).await {
Ok(_) => debug!("Connection deactivated"),
Err(e) => warn!("Failed to deactivate connection: {}", e),
}
}
}
disconnect_wifi_and_wait(conn, &wifi_device, timeout_config).await?;
info!("Disconnected from network");
Ok(())
}
pub(crate) async fn get_device_by_interface(
conn: &Connection,
interface_name: &str,
) -> Result<OwnedObjectPath> {
let nm = NMProxy::new(conn).await?;
let devices = nm.get_devices().await?;
for dev_path in devices {
let dev = NMDeviceProxy::builder(conn)
.path(dev_path.clone())?
.build()
.await?;
if let Ok(iface) = dev.interface().await
&& iface == interface_name
{
debug!("Found device with interface: {}", interface_name);
return Ok(dev_path);
}
}
Err(ConnectionError::NotFound)
}