#[path = "linux_iwd.rs"]
mod linux_iwd;
#[path = "linux_listener.rs"]
mod linux_listener;
#[path = "linux_network_manager.rs"]
mod linux_network_manager;
use crate::error::{WifiError, WifiResult};
use crate::wifi::types::{ConnectionEvent, WifiInfo};
use linux_iwd::IwdBackend;
use linux_network_manager::NetworkManagerBackend;
use secrecy::SecretString;
use std::sync::{Arc, OnceLock, RwLock};
use tokio::sync::mpsc::UnboundedSender;
use zbus::blocking::{Connection, Proxy};
use zbus::zvariant;
use zbus::zvariant::OwnedValue;
pub(crate) const NETWORK_MANAGER_SERVICE: &str = "org.freedesktop.NetworkManager";
pub(crate) const NETWORK_MANAGER_PATH: &str = "/org/freedesktop/NetworkManager";
pub(crate) const NETWORK_MANAGER_INTERFACE: &str = "org.freedesktop.NetworkManager";
pub(crate) const NM_DEVICE_INTERFACE: &str = "org.freedesktop.NetworkManager.Device";
pub(crate) const NM_DEVICE_WIFI_INTERFACE: &str = "org.freedesktop.NetworkManager.Device.Wireless";
pub(crate) const NM_ACCESS_POINT_INTERFACE: &str = "org.freedesktop.NetworkManager.AccessPoint";
pub(crate) const NM_SETTINGS_PATH: &str = "/org/freedesktop/NetworkManager/Settings";
pub(crate) const NM_SETTINGS_INTERFACE: &str = "org.freedesktop.NetworkManager.Settings";
pub(crate) const NM_CONNECTION_INTERFACE: &str =
"org.freedesktop.NetworkManager.Settings.Connection";
pub(crate) const IWD_SERVICE: &str = "net.connman.iwd";
pub(crate) const IWD_PATH: &str = "/net/connman/iwd";
pub(crate) const IWD_STATION_INTERFACE: &str = "net.connman.iwd.Station";
pub(crate) const IWD_DEVICE_INTERFACE: &str = "net.connman.iwd.Device";
pub(crate) const IWD_NETWORK_INTERFACE: &str = "net.connman.iwd.Network";
pub(crate) const IWD_KNOWN_NETWORK_INTERFACE: &str = "net.connman.iwd.KnownNetwork";
pub(crate) const IWD_AGENT_MANAGER_INTERFACE: &str = "net.connman.iwd.AgentManager";
pub(crate) const IWD_AGENT_PATH: &str = "/org/wifui/IwdAgent";
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
pub enum BackendChoice {
Auto,
Nm,
Iwd,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SelectedBackend {
NetworkManager,
Iwd,
}
pub(crate) trait WifiBackend: Send + Sync {
fn name(&self) -> &'static str;
fn listener_spec(&self) -> ListenerSpec;
fn connect_profile(&self, ssid: &str) -> WifiResult<()>;
fn connect_with_password(
&self,
ssid: &str,
password: &SecretString,
auth: &str,
cipher: &str,
hidden: bool,
) -> WifiResult<()>;
fn connect_open(&self, ssid: &str, hidden: bool) -> WifiResult<()>;
fn disconnect(&self) -> WifiResult<()>;
fn disconnect_and_wait(&self) -> WifiResult<()>;
fn get_connected_ssid(&self) -> WifiResult<Option<String>>;
fn get_wifi_networks(&self) -> WifiResult<Vec<WifiInfo>>;
fn scan_networks(&self) -> WifiResult<()>;
#[allow(dead_code)]
fn get_saved_profiles(&self) -> WifiResult<Vec<String>>;
fn set_auto_connect(&self, ssid: &str, enable: bool) -> WifiResult<()>;
fn forget_network(&self, ssid: &str) -> WifiResult<()>;
fn get_wifi_password(&self, ssid: &str) -> WifiResult<Option<SecretString>>;
}
#[derive(Clone, Debug)]
pub(crate) enum ListenerSpec {
NetworkManager { device_path: String },
Iwd { station_path: String },
}
struct BackendStatus {
available: bool,
name: String,
message: String,
}
struct BackendRegistry {
backend: Option<Arc<dyn WifiBackend>>,
status: BackendStatus,
}
impl Default for BackendRegistry {
fn default() -> Self {
Self {
backend: None,
status: BackendStatus {
available: false,
name: "none".to_string(),
message: "Linux Wi-Fi backend unavailable; start NetworkManager or iwd".to_string(),
},
}
}
}
static REGISTRY: OnceLock<RwLock<BackendRegistry>> = OnceLock::new();
fn registry() -> &'static RwLock<BackendRegistry> {
REGISTRY.get_or_init(|| RwLock::new(BackendRegistry::default()))
}
pub(crate) fn system_connection() -> WifiResult<Connection> {
Connection::system().map_err(|e| WifiError::dbus("connect to the system bus", &e))
}
pub(crate) fn new_proxy(
connection: &Connection,
destination: &str,
path: &str,
interface: &str,
) -> WifiResult<Proxy<'static>> {
Proxy::new_owned(
connection.clone(),
destination.to_string(),
path.to_string(),
interface.to_string(),
)
.map_err(|e| WifiError::dbus(&format!("create {interface} proxy"), &e))
}
pub(crate) fn owned_object_path(path: &str) -> WifiResult<zvariant::OwnedObjectPath> {
zvariant::OwnedObjectPath::try_from(path.to_string())
.map_err(|e| WifiError::dbus(&format!("build D-Bus object path ({path})"), &e))
}
pub(crate) fn owned_value<T>(value: T) -> OwnedValue
where
T: Into<zvariant::Value<'static>>,
{
let value: zvariant::Value<'static> = value.into();
OwnedValue::try_from(value)
.expect("values used in NetworkManager settings are valid D-Bus values")
}
pub(crate) fn value_string(value: &OwnedValue) -> Option<String> {
String::try_from(value.clone()).ok()
}
pub(crate) fn value_bytes(value: &OwnedValue) -> Option<Vec<u8>> {
Vec::<u8>::try_from(value.clone()).ok()
}
pub(crate) fn value_bool(value: &OwnedValue) -> Option<bool> {
bool::try_from(value.clone()).ok()
}
fn service_has_owner(connection: &Connection, service: &str) -> WifiResult<bool> {
let proxy = new_proxy(
connection,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
)?;
proxy
.call("NameHasOwner", &service)
.map_err(|e| WifiError::dbus(&format!("check whether {service} is running"), &e))
}
fn selected_backend_for(
choice: BackendChoice,
network_manager_available: bool,
iwd_available: bool,
) -> Option<SelectedBackend> {
match choice {
BackendChoice::Auto => {
if network_manager_available {
Some(SelectedBackend::NetworkManager)
} else if iwd_available {
Some(SelectedBackend::Iwd)
} else {
None
}
}
BackendChoice::Nm if network_manager_available => Some(SelectedBackend::NetworkManager),
BackendChoice::Iwd if iwd_available => Some(SelectedBackend::Iwd),
_ => None,
}
}
fn backend_unavailable_for(choice: BackendChoice) -> WifiError {
let backend = match choice {
BackendChoice::Auto => "NetworkManager or iwd",
BackendChoice::Nm => "NetworkManager",
BackendChoice::Iwd => "iwd",
};
WifiError::BackendUnavailable {
backend: backend.to_string(),
}
}
pub fn initialize_backend(
choice: BackendChoice,
target_interface: Option<&str>,
) -> WifiResult<&'static str> {
let connection = system_connection()?;
let network_manager_available = service_has_owner(&connection, NETWORK_MANAGER_SERVICE)?;
let iwd_available = service_has_owner(&connection, IWD_SERVICE)?;
if choice == BackendChoice::Auto {
let mut last_error = None;
if network_manager_available {
match NetworkManagerBackend::discover(&connection, target_interface) {
Ok(backend) => return store_backend(Arc::new(backend)),
Err(error) => last_error = Some(error),
}
}
if iwd_available {
match IwdBackend::discover(&connection, target_interface) {
Ok(backend) => return store_backend(Arc::new(backend)),
Err(error) => last_error = Some(error),
}
}
return Err(last_error.unwrap_or_else(|| backend_unavailable_for(choice)));
}
let selected = selected_backend_for(choice, network_manager_available, iwd_available)
.ok_or_else(|| backend_unavailable_for(choice))?;
let backend: Arc<dyn WifiBackend> = match selected {
SelectedBackend::NetworkManager => Arc::new(NetworkManagerBackend::discover(
&connection,
target_interface,
)?),
SelectedBackend::Iwd => Arc::new(IwdBackend::discover(&connection, target_interface)?),
};
store_backend(backend)
}
fn store_backend(backend: Arc<dyn WifiBackend>) -> WifiResult<&'static str> {
let name = backend.name();
let mut state = registry()
.write()
.map_err(|_| WifiError::Internal("Linux backend registry was poisoned".to_string()))?;
state.backend = Some(backend);
state.status = BackendStatus {
available: true,
name: name.to_string(),
message: format!("Linux backend: {name}"),
};
Ok(name)
}
pub(crate) fn active_backend() -> WifiResult<Arc<dyn WifiBackend>> {
let state = registry()
.read()
.map_err(|_| WifiError::Internal("Linux backend registry was poisoned".to_string()))?;
state
.backend
.clone()
.ok_or_else(|| WifiError::BackendUnavailable {
backend: "Linux Wi-Fi".to_string(),
})
}
pub fn is_backend_available() -> bool {
registry()
.read()
.map(|state| state.status.available)
.unwrap_or(false)
}
pub fn backend_name() -> String {
registry()
.read()
.map(|state| state.status.name.clone())
.unwrap_or_else(|_| "none".to_string())
}
pub fn backend_unavailable_message() -> String {
registry()
.read()
.map(|state| state.status.message.clone())
.unwrap_or_else(|_| "Linux Wi-Fi backend unavailable".to_string())
}
pub struct WifiListener {
_inner: linux_listener::WifiListener,
}
impl std::fmt::Debug for WifiListener {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.debug_tuple("WifiListener").finish()
}
}
pub fn start_wifi_listener(sender: UnboundedSender<ConnectionEvent>) -> WifiResult<WifiListener> {
let backend = active_backend()?;
Ok(WifiListener {
_inner: linux_listener::start(backend.listener_spec(), sender)?,
})
}
pub fn connect_profile(ssid: &str) -> WifiResult<()> {
active_backend()?.connect_profile(ssid)
}
pub fn connect_with_password(
ssid: &str,
password: &SecretString,
auth: &str,
cipher: &str,
hidden: bool,
) -> WifiResult<()> {
active_backend()?.connect_with_password(ssid, password, auth, cipher, hidden)
}
pub fn connect_open(ssid: &str, hidden: bool) -> WifiResult<()> {
active_backend()?.connect_open(ssid, hidden)
}
pub fn disconnect() -> WifiResult<()> {
active_backend()?.disconnect()
}
pub fn disconnect_and_wait() -> WifiResult<()> {
active_backend()?.disconnect_and_wait()
}
pub fn get_connected_ssid() -> WifiResult<Option<String>> {
active_backend()?.get_connected_ssid()
}
pub fn get_wifi_networks() -> WifiResult<Vec<WifiInfo>> {
active_backend()?.get_wifi_networks()
}
pub fn scan_networks() -> WifiResult<()> {
active_backend()?.scan_networks()
}
#[allow(dead_code)]
pub fn get_saved_profiles() -> WifiResult<Vec<String>> {
active_backend()?.get_saved_profiles()
}
pub fn set_auto_connect(ssid: &str, enable: bool) -> WifiResult<()> {
active_backend()?.set_auto_connect(ssid, enable)
}
pub fn forget_network(ssid: &str) -> WifiResult<()> {
active_backend()?.forget_network(ssid)
}
pub fn get_wifi_password(ssid: &str) -> WifiResult<Option<SecretString>> {
active_backend()?.get_wifi_password(ssid)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auto_selection_prefers_network_manager() {
assert_eq!(
selected_backend_for(BackendChoice::Auto, true, true),
Some(SelectedBackend::NetworkManager)
);
}
#[test]
fn explicit_selection_does_not_fall_back() {
assert_eq!(selected_backend_for(BackendChoice::Nm, false, true), None);
assert_eq!(selected_backend_for(BackendChoice::Iwd, true, false), None);
}
#[test]
fn auto_selection_reports_no_daemon() {
assert_eq!(
selected_backend_for(BackendChoice::Auto, false, false),
None
);
}
}