steamos-manager/src/wifi.rs
Vicki Pfau 4c81c92586 wifi: Properly parse all NM config files for backend
Previously there was a half-baked parser that looked for a fixed value in a
fixed place, but this was not robust. This approach is properly robust, in case
various different files set or override it.
2024-08-29 20:12:45 -07:00

417 lines
13 KiB
Rust

/*
* Copyright © 2023 Collabora Ltd.
* Copyright © 2024 Valve Software
*
* SPDX-License-Identifier: MIT
*/
use anyhow::{anyhow, bail, ensure, Error, Result};
use config::builder::AsyncState;
use config::{ConfigBuilder, FileFormat};
use std::str::FromStr;
use strum::{Display, EnumString};
use tokio::fs;
use tracing::error;
use zbus::Connection;
use crate::process::{run_script, script_output};
use crate::systemd::{daemon_reload, SystemdUnit};
use crate::{path, read_config_directory};
const OVERRIDE_CONTENTS: &str = "[Service]
ExecStart=
ExecStart=/usr/lib/iwd/iwd -d
";
const OVERRIDE_FOLDER: &str = "/etc/systemd/system/iwd.service.d";
const OVERRIDE_PATH: &str = "/etc/systemd/system/iwd.service.d/99-valve-override.conf";
// Only use one path for output for now. If needed we can add a timestamp later
// to have multiple files, etc.
const OUTPUT_FILE: &str = "/var/log/wifitrace.dat";
const TRACE_CMD_PATH: &str = "/usr/bin/trace-cmd";
const MIN_BUFFER_SIZE: u32 = 100;
const WIFI_BACKEND_PATHS: &[&str] = &[
"/usr/lib/etc/NetworkManager/conf.d",
"/etc/NetworkManager/conf.d",
];
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[repr(u32)]
pub enum WifiDebugMode {
#[strum(
to_string = "off",
serialize = "disable",
serialize = "disabled",
serialize = "0"
)]
Off = 0,
Tracing = 1,
}
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
#[strum(ascii_case_insensitive)]
#[repr(u32)]
pub enum WifiPowerManagement {
#[strum(
to_string = "disabled",
serialize = "off",
serialize = "disable",
serialize = "0"
)]
Disabled = 0,
#[strum(
to_string = "enabled",
serialize = "on",
serialize = "enable",
serialize = "1"
)]
Enabled = 1,
}
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[repr(u32)]
pub enum WifiBackend {
Iwd = 0,
WPASupplicant = 1,
}
impl TryFrom<u32> for WifiDebugMode {
type Error = Error;
fn try_from(v: u32) -> Result<Self, Self::Error> {
match v {
x if x == WifiDebugMode::Off as u32 => Ok(WifiDebugMode::Off),
x if x == WifiDebugMode::Tracing as u32 => Ok(WifiDebugMode::Tracing),
_ => Err(anyhow!("No enum match for value {v}")),
}
}
}
impl TryFrom<u32> for WifiPowerManagement {
type Error = Error;
fn try_from(v: u32) -> Result<Self, Self::Error> {
match v {
x if x == WifiPowerManagement::Disabled as u32 => Ok(WifiPowerManagement::Disabled),
x if x == WifiPowerManagement::Enabled as u32 => Ok(WifiPowerManagement::Enabled),
_ => Err(anyhow!("No enum match for value {v}")),
}
}
}
impl TryFrom<u32> for WifiBackend {
type Error = Error;
fn try_from(v: u32) -> Result<Self, Self::Error> {
match v {
x if x == WifiBackend::Iwd as u32 => Ok(WifiBackend::Iwd),
x if x == WifiBackend::WPASupplicant as u32 => Ok(WifiBackend::WPASupplicant),
_ => Err(anyhow!("No enum match for WifiBackend value {v}")),
}
}
}
pub(crate) async fn setup_iwd_config(want_override: bool) -> std::io::Result<()> {
// Copy override.conf file into place or out of place depending
// on install value
if want_override {
// Copy it in
// Make sure the folder exists
fs::create_dir_all(path(OVERRIDE_FOLDER)).await?;
// Then write the contents into the file
fs::write(path(OVERRIDE_PATH), OVERRIDE_CONTENTS).await
} else {
// Delete it
match fs::remove_file(path(OVERRIDE_PATH)).await {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
res => res,
}
}
}
async fn restart_iwd(connection: Connection) -> Result<()> {
// First reload systemd since we modified the config most likely
// otherwise we wouldn't be restarting iwd.
daemon_reload(&connection)
.await
.inspect_err(|message| error!("restart_iwd: reload systemd got an error: {message}"))?;
// worked, now restart iwd
let unit = SystemdUnit::new(connection, "iwd.service").await?;
unit.restart()
.await
.inspect_err(|message| error!("restart_iwd: restart unit got an error: {message}"))
}
async fn stop_tracing() -> Result<()> {
// Stop tracing and extract ring buffer to disk for capture
run_script(TRACE_CMD_PATH, &["stop"]).await?;
// stop tracing worked
run_script(TRACE_CMD_PATH, &["extract", "-o", OUTPUT_FILE]).await
}
async fn start_tracing(buffer_size: u32) -> Result<()> {
// Start tracing
let size_str = buffer_size.to_string();
run_script(
TRACE_CMD_PATH,
&["start", "-e", "ath11k_wmi_diag", "-b", &size_str],
)
.await
}
pub(crate) async fn set_wifi_debug_mode(
mode: WifiDebugMode,
buffer_size: u32,
should_trace: bool,
connection: Connection,
) -> Result<()> {
match get_wifi_backend().await {
Ok(WifiBackend::Iwd) => (),
Ok(backend) => bail!("Setting Wi-Fi debug mode not supported with backend {backend}"),
Err(e) => return Err(e),
}
match mode {
WifiDebugMode::Off => {
// If mode is 0 disable wifi debug mode
// Stop any existing trace and flush to disk.
if should_trace {
if let Err(message) = stop_tracing().await {
bail!("stop_tracing command got an error: {message}");
};
}
// Stop_tracing was successful
if let Err(message) = setup_iwd_config(false).await {
bail!("setup_iwd_config false got an error: {message}");
};
// setup_iwd_config false worked
if let Err(message) = restart_iwd(connection).await {
bail!("restart_iwd got an error: {message}");
};
}
WifiDebugMode::Tracing => {
ensure!(buffer_size > MIN_BUFFER_SIZE, "Buffer size too small");
if let Err(message) = setup_iwd_config(true).await {
bail!("setup_iwd_config true got an error: {message}");
}
// setup_iwd_config worked
if let Err(message) = restart_iwd(connection).await {
bail!("restart_iwd got an error: {message}");
};
// restart_iwd worked
if should_trace {
if let Err(message) = start_tracing(buffer_size).await {
bail!("start_tracing got an error: {message}");
};
}
}
}
Ok(())
}
pub(crate) async fn get_wifi_backend() -> Result<WifiBackend> {
let mut builder = ConfigBuilder::<AsyncState>::default();
for dir in WIFI_BACKEND_PATHS {
println!("{dir}");
builder = read_config_directory(builder, path(dir), &["conf"], FileFormat::Ini).await?;
}
println!("{builder:?}");
let config = builder.build().await?;
println!("{config:?}");
if let Some(backend) = config.get_table("device")?.remove("wifi.backend") {
let backend = backend.into_string()?;
println!("{backend:?}");
return Ok(WifiBackend::from_str(backend.as_str())?);
}
bail!("Wi-Fi backend not found in config");
}
pub(crate) async fn set_wifi_backend(backend: WifiBackend) -> Result<()> {
run_script("/usr/bin/steamos-wifi-set-backend", &[backend.to_string()]).await
}
pub(crate) async fn list_wifi_interfaces() -> Result<Vec<String>> {
let output = script_output("/usr/bin/iw", &["dev"]).await?;
Ok(output
.lines()
.filter_map(|line| match line.trim().split_once(' ') {
Some(("Interface", name)) => Some(name.to_string()),
_ => None,
})
.collect())
}
pub(crate) async fn get_wifi_power_management_state() -> Result<WifiPowerManagement> {
let mut found_any = false;
for iface in list_wifi_interfaces().await? {
let output =
script_output("/usr/bin/iw", &["dev", iface.as_str(), "get", "power_save"]).await?;
for line in output.lines() {
match line.trim() {
"Power save: on" => return Ok(WifiPowerManagement::Enabled),
"Power save: off" => found_any = true,
_ => continue,
}
}
}
ensure!(found_any, "No interfaces found");
Ok(WifiPowerManagement::Disabled)
}
pub(crate) async fn set_wifi_power_management_state(state: WifiPowerManagement) -> Result<()> {
let state = match state {
WifiPowerManagement::Disabled => "off",
WifiPowerManagement::Enabled => "on",
};
for iface in list_wifi_interfaces().await? {
run_script(
"/usr/bin/iw",
&["dev", iface.as_str(), "set", "power_save", state],
)
.await
.inspect_err(|message| error!("Error setting Wi-Fi power management state: {message}"))?;
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::{enum_on_off, enum_roundtrip, testing};
use tokio::fs::{create_dir_all, read_to_string, remove_dir, try_exists, write};
#[test]
fn test_wifi_backend_to_string() {
assert_eq!(WifiBackend::Iwd.to_string(), "iwd");
assert_eq!(WifiBackend::WPASupplicant.to_string(), "wpa_supplicant");
}
#[tokio::test]
async fn test_setup_iwd_config() {
let _h = testing::start();
// Remove with no dir
assert!(setup_iwd_config(false).await.is_ok());
create_dir_all(path(OVERRIDE_FOLDER))
.await
.expect("create_dir_all");
// Remove with dir but no file
assert!(setup_iwd_config(false).await.is_ok());
// Remove with dir and file
write(path(OVERRIDE_PATH), "").await.expect("write");
assert!(try_exists(path(OVERRIDE_PATH)).await.unwrap());
assert!(setup_iwd_config(false).await.is_ok());
assert!(!try_exists(path(OVERRIDE_PATH)).await.unwrap());
// Double remove
assert!(setup_iwd_config(false).await.is_ok());
// Create with no dir
remove_dir(path(OVERRIDE_FOLDER)).await.expect("remove_dir");
assert!(setup_iwd_config(true).await.is_ok());
assert_eq!(
read_to_string(path(OVERRIDE_PATH)).await.unwrap(),
OVERRIDE_CONTENTS
);
// Create with dir
assert!(setup_iwd_config(false).await.is_ok());
assert!(setup_iwd_config(true).await.is_ok());
assert_eq!(
read_to_string(path(OVERRIDE_PATH)).await.unwrap(),
OVERRIDE_CONTENTS
);
}
#[tokio::test]
async fn test_get_wifi_backend() {
let _h = testing::start();
for dir in WIFI_BACKEND_PATHS {
create_dir_all(path(dir)).await.expect("create_dir_all");
}
assert!(get_wifi_backend().await.is_err());
write(path(WIFI_BACKEND_PATHS[0]).join("test.conf"), "[device]")
.await
.expect("write");
assert!(get_wifi_backend().await.is_err());
write(
path(WIFI_BACKEND_PATHS[0]).join("test.conf"),
"[device]\nwifi.backend=fake\n",
)
.await
.expect("write");
assert!(get_wifi_backend().await.is_err());
write(
path(WIFI_BACKEND_PATHS[0]).join("test.conf"),
"[device]\nwifi.backend=iwd\n",
)
.await
.expect("write");
assert_eq!(get_wifi_backend().await.unwrap(), WifiBackend::Iwd);
write(
path(WIFI_BACKEND_PATHS[0]).join("test.conf"),
"[device]\nwifi.backend=wpa_supplicant\n",
)
.await
.expect("write");
assert_eq!(
get_wifi_backend().await.unwrap(),
WifiBackend::WPASupplicant
);
}
#[test]
fn wifi_debug_mode_roundtrip() {
enum_roundtrip!(WifiDebugMode {
0: u32 = Off,
1: u32 = Tracing,
"off": str = Off,
"tracing": str = Tracing,
});
assert!(WifiDebugMode::try_from(2).is_err());
assert!(WifiDebugMode::from_str("onf").is_err());
}
#[test]
fn wifi_power_management_roundtrip() {
enum_roundtrip!(WifiPowerManagement {
0: u32 = Disabled,
1: u32 = Enabled,
"disabled": str = Disabled,
"enabled": str = Enabled,
});
enum_on_off!(WifiPowerManagement => (Enabled, Disabled));
assert!(WifiPowerManagement::try_from(2).is_err());
assert!(WifiPowerManagement::from_str("onf").is_err());
}
#[test]
fn wifi_backend_roundtrip() {
enum_roundtrip!(WifiBackend {
0: u32 = Iwd,
1: u32 = WPASupplicant,
"iwd": str = Iwd,
"wpa_supplicant": str = WPASupplicant,
});
assert!(WifiBackend::try_from(2).is_err());
assert!(WifiBackend::from_str("iwl").is_err());
}
}