wayle-network 0.1.2

WiFi and wired network management
Documentation
use std::collections::HashMap;

use tracing::{debug, instrument};
use zbus::{
    Connection,
    zvariant::{OwnedObjectPath, OwnedValue, Value},
};

use crate::{
    core::{
        access_point::types::{SecurityType, Ssid},
        settings::Settings,
    },
    error::Error,
    proxy::{access_point::AccessPointProxy, devices::DeviceProxy, manager::NetworkManagerProxy},
    types::flags::{NM80211ApFlags, NM80211ApSecurityFlags},
};

type ConnectionSettings = HashMap<String, HashMap<String, OwnedValue>>;

// List of manufacturer default SSIDs that should be locked to BSSID
// to prevent connecting to neighbors' routers with the same default name.
// Inspired by nm-applet's approach to handling duplicate SSIDs.
const MANUFACTURER_DEFAULT_SSIDS: &[&str] = &[
    "linksys",
    "linksys-a",
    "linksys-g",
    "default",
    "belkin54g",
    "NETGEAR",
    "o2DSL",
    "WLAN",
    "ALICE-WLAN",
];

pub(super) struct WifiControls;

impl WifiControls {
    #[instrument(skip(connection), fields(enabled = enabled), err)]
    pub(super) async fn set_enabled(connection: &Connection, enabled: bool) -> Result<(), Error> {
        let proxy = NetworkManagerProxy::new(connection).await?;

        proxy
            .set_wireless_enabled(enabled)
            .await
            .map_err(|e| Error::OperationFailed {
                operation: "set wireless enabled",
                source: e.into(),
            })?;

        Ok(())
    }

    #[instrument(skip(connection), fields(device = %device_path), err)]
    pub(super) async fn disconnect(
        connection: &Connection,
        device_path: &str,
    ) -> Result<(), Error> {
        let proxy = NetworkManagerProxy::new(connection).await?;

        let device_proxy = DeviceProxy::new(connection, device_path)
            .await
            .map_err(|e| Error::OperationFailed {
                operation: "create device proxy",
                source: e.into(),
            })?;

        let active_connection_path =
            device_proxy
                .active_connection()
                .await
                .map_err(|e| Error::OperationFailed {
                    operation: "get active connection",
                    source: e.into(),
                })?;

        if active_connection_path.as_str() == "/" || active_connection_path.as_str().is_empty() {
            return Ok(());
        }

        proxy
            .deactivate_connection(&active_connection_path)
            .await
            .map_err(|e| Error::OperationFailed {
                operation: "deactivate connection",
                source: e.into(),
            })?;

        Ok(())
    }

    #[instrument(
        skip(connection, password, settings),
        fields(device = %device_path, ap = %ap_path),
        err
    )]
    pub(super) async fn connect(
        connection: &Connection,
        device_path: &str,
        ap_path: OwnedObjectPath,
        password: Option<String>,
        settings: &Settings,
    ) -> Result<OwnedObjectPath, Error> {
        let proxy = NetworkManagerProxy::new(connection).await?;

        let ap_proxy = AccessPointProxy::new(connection, ap_path.clone())
            .await
            .map_err(|err| Error::OperationFailed {
                operation: "create access point proxy",
                source: err.into(),
            })?;

        let ssid_bytes = ap_proxy
            .ssid()
            .await
            .map_err(|err| Error::OperationFailed {
                operation: "get ssid",
                source: err.into(),
            })?;

        let ssid = Ssid::new(ssid_bytes.clone());
        let ssid_string = ssid.to_string_lossy();

        let device_path =
            OwnedObjectPath::try_from(device_path).map_err(|err| Error::DbusError(err.into()))?;

        let existing_profiles = settings.connections_for_ssid(&ssid);

        if let Some(profile) = existing_profiles.first() {
            debug!(
                profile = %profile.object_path,
                ssid = %ssid_string,
                "reusing saved connection profile"
            );

            if let Some(pwd) = password {
                let security_type = Self::detect_security_type(&ap_proxy).await;
                Self::update_profile_password(profile, pwd, security_type).await?;
            }

            let active_connection_path = proxy
                .activate_connection(&profile.object_path, &device_path, &ap_path)
                .await
                .map_err(|err| Error::OperationFailed {
                    operation: "activate existing connection",
                    source: err.into(),
                })?;

            return Ok(active_connection_path);
        }

        debug!(ssid = %ssid_string, "no saved profile found, creating new connection");

        let bssid = if Self::is_manufacturer_default(&ssid_string) {
            ap_proxy.hw_address().await.ok()
        } else {
            None
        };

        let security_type = Self::detect_security_type(&ap_proxy).await;

        let connection_settings = Self::build_connection_settings(
            ssid_string,
            ssid_bytes,
            bssid,
            password,
            security_type,
        )?;

        let (_settings_path, active_connection_path) = proxy
            .add_and_activate_connection(connection_settings, &device_path, &ap_path)
            .await
            .map_err(|err| Error::OperationFailed {
                operation: "add and activate connection",
                source: err.into(),
            })?;

        Ok(active_connection_path)
    }

    async fn update_profile_password(
        profile: &crate::core::settings_connection::ConnectionSettings,
        password: String,
        security_type: SecurityType,
    ) -> Result<(), Error> {
        let mut current_settings = profile.get_settings().await?;

        let security = current_settings
            .entry(String::from("802-11-wireless-security"))
            .or_default();

        let to_owned = |value: Value| {
            value.try_to_owned().map_err(|err| Error::OperationFailed {
                operation: "convert to owned value",
                source: err.into(),
            })
        };

        let key_mgmt = Self::key_mgmt_for_security_type(security_type);
        security.insert(String::from("key-mgmt"), to_owned(Value::from(key_mgmt))?);

        match security_type {
            SecurityType::Wep => {
                security.insert(String::from("wep-key0"), to_owned(Value::from(password))?);
            }
            _ => {
                security.insert(String::from("psk"), to_owned(Value::from(password))?);
            }
        }

        profile.update(current_settings).await?;

        Ok(())
    }

    fn is_manufacturer_default(ssid: &str) -> bool {
        MANUFACTURER_DEFAULT_SSIDS.contains(&ssid)
    }

    fn build_connection_settings(
        ssid_string: String,
        ssid_bytes: Vec<u8>,
        bssid: Option<String>,
        password: Option<String>,
        security_type: SecurityType,
    ) -> Result<ConnectionSettings, Error> {
        let to_owned = |value: Value| {
            value.try_to_owned().map_err(|e| Error::OperationFailed {
                operation: "convert to owned value",
                source: e.into(),
            })
        };

        let mut settings = HashMap::new();

        let mut connection = HashMap::new();
        connection.insert(
            String::from("type"),
            to_owned(Value::from("802-11-wireless"))?,
        );
        connection.insert(String::from("id"), to_owned(Value::from(ssid_string))?);
        settings.insert(String::from("connection"), connection);

        let mut wireless = HashMap::new();
        wireless.insert(String::from("ssid"), to_owned(Value::from(ssid_bytes))?);

        if let Some(bssid_str) = bssid {
            let mac_bytes: Result<Vec<u8>, _> = bssid_str
                .split(':')
                .map(|part| u8::from_str_radix(part, 16))
                .collect();

            if let Ok(bytes) = mac_bytes {
                wireless.insert(String::from("bssid"), to_owned(Value::from(bytes))?);
            }
        }

        settings.insert(String::from("802-11-wireless"), wireless);

        if let Some(pwd) = password {
            let mut security = HashMap::new();
            let key_mgmt = Self::key_mgmt_for_security_type(security_type);
            security.insert(String::from("key-mgmt"), to_owned(Value::from(key_mgmt))?);

            match security_type {
                SecurityType::Wep => {
                    security.insert(String::from("wep-key0"), to_owned(Value::from(pwd))?);
                }
                _ => {
                    security.insert(String::from("psk"), to_owned(Value::from(pwd))?);
                }
            }

            settings.insert(String::from("802-11-wireless-security"), security);
        }

        Ok(settings)
    }

    async fn detect_security_type(ap_proxy: &AccessPointProxy<'_>) -> SecurityType {
        let flags = ap_proxy
            .flags()
            .await
            .map(NM80211ApFlags::from_bits_truncate)
            .unwrap_or(NM80211ApFlags::NONE);

        let wpa_flags = ap_proxy
            .wpa_flags()
            .await
            .map(NM80211ApSecurityFlags::from_bits_truncate)
            .unwrap_or(NM80211ApSecurityFlags::NONE);

        let rsn_flags = ap_proxy
            .rsn_flags()
            .await
            .map(NM80211ApSecurityFlags::from_bits_truncate)
            .unwrap_or(NM80211ApSecurityFlags::NONE);

        SecurityType::from_flags(flags, wpa_flags, rsn_flags)
    }

    fn key_mgmt_for_security_type(security_type: SecurityType) -> &'static str {
        match security_type {
            SecurityType::None => "none",
            SecurityType::Wep => "none",
            SecurityType::Wpa | SecurityType::Wpa2 => "wpa-psk",
            SecurityType::Wpa3 => "sae",
            SecurityType::Enterprise => "wpa-eap",
        }
    }
}