Skip to content

Commit

Permalink
Fix wifi data gathering
Browse files Browse the repository at this point in the history
This fix adds on to 58c0626 which fixed the missing wifi device data
with the unintended side effect of breaking the ability to set wifi
settings from ops. The original problem was that for routers missing
the ifname setting in /etc/config/wireless, the wifi interface names
differ between uci and iw, the two places we gather different wifi
info to be sent over in the WifiDevice field of HardwareInfo.
The first fix used iw instead to gather interface names, but since
ifnames may differ between the two systems if routers are missing the
config line, this broke wifi settings when routers were searching uci
for an interface name only used by iw.

This change calls both uci and iw so no matter whether the router has
its ifname nicknamed it can gather the required data to be sent to ops
for each interface.
  • Loading branch information
ch-iara committed Jan 8, 2025
1 parent 21b50f8 commit 6d62c60
Showing 1 changed file with 215 additions and 40 deletions.
255 changes: 215 additions & 40 deletions althea_kernel_interface/src/hardware_info.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::file_io::get_lines;
use crate::is_openwrt::is_openwrt;
use crate::manipulate_uci::get_uci_var;
use crate::KernelInterfaceError as Error;
use althea_types::extract_wifi_station_data;
Expand All @@ -11,6 +12,8 @@ use althea_types::SensorReading;
use althea_types::WifiDevice;
use althea_types::WifiStationData;
use althea_types::WifiSurveyData;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::fs::File;
use std::io::BufRead;
Expand Down Expand Up @@ -338,14 +341,14 @@ fn get_wifi_devices() -> Vec<WifiDevice> {
match parse_wifi_device_names() {
Ok(devices) => {
let mut wifi_devices = Vec::new();
for dev in devices {
for (iw_dev, uci_dev) in devices {
let wifi_device = WifiDevice {
name: dev.clone(),
survey_data: get_wifi_survey_info(&dev),
station_data: get_wifi_station_info(&dev),
ssid: get_radio_ssid(&dev),
channel: get_radio_channel(&dev),
enabled: get_radio_enabled(&dev),
name: uci_dev.clone(),
survey_data: get_wifi_survey_info(&iw_dev),
station_data: get_wifi_station_info(&iw_dev),
ssid: get_radio_ssid(&uci_dev),
channel: get_radio_channel(&uci_dev),
enabled: get_radio_enabled(&uci_dev),
};
wifi_devices.push(wifi_device);
info!("wifi {:?}", wifi_devices); // Log output at each iteration
Expand Down Expand Up @@ -400,16 +403,59 @@ fn get_conntrack_info() -> Option<ConntrackInfo> {
Some(ret)
}

/// Device names are in the form wlan0, wlan1 etc
fn parse_wifi_device_names() -> Result<Vec<String>, Error> {
/// this function parses the iw names and the uci names for the same interfaces
/// and pairs them so that the data returned for a WifiDevice contains all the
/// information required whether the device has a set ifname nickname in its
/// /etc/config/wireless file or not. This lack of nickname caused wifi information
/// sent to ops to be incomplete on either the wifi settings modal or the radios &
/// wifi devices section of the router details page, depending on which place we were
/// getting the device names from here. The returned Vec is (uci name, iw name) pairs
fn parse_wifi_device_names() -> Result<HashMap<String, String>, Error> {
let iw_ifnames = parse_iw_wifi_device_names()?;
let uci_ifnames = parse_uci_wifi_device_names()?;
Ok(pair_iw_and_uci(&iw_ifnames, &uci_ifnames))
}

/// Pairs a single uci and iw interface based on channels: since interface naming convention on iw can vary based
/// on whether or not a default has been set in /etc/config/wireless, channel numbers are instead used to match outputs.
/// Returns a hashmap of iw interface names to uci interface names
fn pair_iw_and_uci(
iw_ifnames: &HashMap<String, String>,
uci_ifnames: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut pairs = HashMap::new();
if is_openwrt() || cfg!(test) {
for (iw_ifname, iw_channel) in iw_ifnames {
for (uci_ifname, uci_channel) in uci_ifnames {
if iw_channel == uci_channel {
pairs.insert(iw_ifname.clone(), uci_ifname.clone());
}
}
}} else {
// more data needed for non-openwrt devices- for now, just pair down the line.
// it's not ideal but on the ops side we can see the interface names data is coming
// from and adjust accordingly. Uci and iw data is displayed separately on ops
let mut uci_iter = uci_ifnames.keys();
for iw_ifname in iw_ifnames.keys() {
if let Some(uci_ifname) = uci_iter.next() {
pairs.insert(iw_ifname.clone(), uci_ifname.clone());
}
}
}
pairs
}

/// Device names are in the form wlan0, wlan1 etc when set by etc/config/wireless but can vary otherwise.
/// This function returns a map of radio names to their respective channel allocations as seen by 'iw dev'
fn parse_iw_wifi_device_names() -> Result<HashMap<String, String>, Error> {
// Call iw dev to get a list of wifi interfaces
let res = Command::new("iw")
.args(["dev"])
.stdout(Stdio::piped())
.output();
match res {
Ok(a) => match String::from_utf8(a.stdout) {
Ok(a) => Ok(extract_wifi_ifnames(&a)),
Ok(a) => Ok(extract_iw_ifnames(&a)),
Err(e) => {
error!("Unable to parse iw dev output {:?}", e);
Err(Error::FromUtf8Error)
Expand All @@ -419,22 +465,79 @@ fn parse_wifi_device_names() -> Result<Vec<String>, Error> {
}
}

fn extract_wifi_ifnames(dev_output: &str) -> Vec<String> {
let mut ret: Vec<String> = vec![];

// we are looking for the line "Interface [ifname]"
let mut iter = dev_output.split_ascii_whitespace();
loop {
let to_struct = iter.next();
if let Some(to_struct) = to_struct {
if to_struct == "Interface" {
let ifname = iter.next();
if let Some(ifname) = ifname {
ret.push(ifname.to_string());
/// Returns a map of radio names with their respective channel allocations as seen by 'uci show wireless'
fn parse_uci_wifi_device_names() -> Result<HashMap<String, String>, Error> {
// We parse /etc/config/wireless which is an openwrt config. We return an error if not openwrt
if is_openwrt() {
let res = Command::new("uci")
.args(["show", "wireless"])
.stdout(Stdio::piped())
.output();
match res {
Ok(a) => match String::from_utf8(a.stdout) {
Ok(a) => Ok(extract_uci_ifnames(&a)),
Err(e) => {
error!("Unable to parse uci show wireless output {:?}", e);
Err(Error::FromUtf8Error)
}
},
Err(e) => Err(Error::ParseError(e.to_string())),
}
} else {
// Fallback to /proc/ parsing if no openwrt
let mut ret = HashMap::new();
let path = "/proc/net/wireless";
let lines = get_lines(path)?;
for line in lines {
if line.contains(':') {
let name: Vec<&str> = line.split(':').collect();
let name = name[0];
let name = name.replace(' ', "");
ret.insert(name.to_string(), String::new());
}
}
Ok(ret)
}
}

fn extract_iw_ifnames(dev_output: &str) -> HashMap<String, String> {
let mut ret: HashMap<String, String> = HashMap::new();
let mut current_interface = None;
// we are looking for the line "Interface [ifname]" and the line "channel [channel] xyz"
for line in dev_output.lines() {
if line.trim_start().starts_with("Interface") {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(interface) = parts.get(1) {
current_interface = Some(interface.to_string());
}
} else if let Some(interface) = &current_interface {
if line.trim_start().starts_with("channel") {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(channel) = parts.get(1) {
ret.insert(interface.clone(), channel.to_string());
}
}
}
}
ret
}

fn extract_uci_ifnames(dev_output: &str) -> HashMap<String, String> {
let mut ret: HashMap<String, String> = HashMap::new();
let mut interfaces = HashSet::new();
// ex. we are looking for the line "wireless.radio1=wifi-device" to extract "radio1"
for line in dev_output.lines() {
if let Some((key, value)) = line
.strip_prefix("wireless.")
.and_then(|line| line.split_once("="))
{
if value == "wifi-device" {
interfaces.insert(key.to_string());
} else if let Some((name, property)) = key.rsplit_once('.') {
if property == "channel" && interfaces.contains(name) {
ret.insert(name.to_string(), value.trim_matches('\'').to_string());
}
}
} else {
break;
}
}
ret
Expand Down Expand Up @@ -707,34 +810,106 @@ mod test {
#[test]
fn test_parse_wifi_device_names() {
// sample output from iw dev
let iw_dev_output = "phy#1
Interface wlan1
ifindex 12
wdev 0x100000002
addr 12:23:34:45:56:67
ssid altheahome-5
let iw_dev_output = "
phy#2
Interface phy2-ap0
ifindex 10
wdev 0x200000002
addr c4:41:1e:2a:b8:f8
ssid AltheaHome-5
type AP
channel 36 (5180 MHz), width: 80 MHz, center1: 5210 MHz
txpower 23.00 dBm
multicast TXQ:
qsz-byt qsz-pkt flows drops marks overlmt hashcol tx-bytes tx-packets
0 0 3991833 0 0 0 0 1112061710 3991837
0 0 0 0 0 0 0 0 0
phy#1
Interface phy1-ap0
ifindex 9
wdev 0x100000002
addr c4:41:1e:2a:b8:f7
ssid AltheaHome-2.4
type AP
channel 1 (2412 MHz), width: 20 MHz, center1: 2412 MHz
txpower 30.00 dBm
multicast TXQ:
qsz-byt qsz-pkt flows drops marks overlmt hashcol tx-bytes tx-packets
0 0 0 0 0 0 0 0 0
phy#0
Interface wlan0
Interface phy0-ap0
ifindex 11
wdev 0x2
addr 76:65:54:43:32:21
ssid altheahome-2.4
addr c4:41:1e:2a:b8:f9
ssid AltheaHome-5
type AP
channel 11 (2462 MHz), width: 20 MHz, center1: 2462 MHz
channel 149 (5745 MHz), width: 80 MHz, center1: 5775 MHz
txpower 30.00 dBm
multicast TXQ:
qsz-byt qsz-pkt flows drops marks overlmt hashcol tx-bytes tx-packets
0 0 3991759 0 0 0 3 1112047714 3991791
0 0 0 0 0 0 0 0 0
";
let res = extract_wifi_ifnames(iw_dev_output);
assert!(res.len() == 2);
assert!(res.contains(&"wlan0".to_string()));
assert!(res.contains(&"wlan1".to_string()));
// sample output from uci show wireless
let uci_dev_output = "
wireless.radio0=wifi-device
wireless.radio0.type='mac80211'
wireless.radio0.path='soc/40000000.pci/pci0000:00/0000:00:00.0/0000:01:00.0'
wireless.radio0.channel='149'
wireless.radio0.band='5g'
wireless.radio0.htmode='VHT80'
wireless.radio0.disabled='0'
wireless.default_radio0=wifi-iface
wireless.default_radio0.device='radio0'
wireless.default_radio0.network='lan'
wireless.default_radio0.mode='ap'
wireless.default_radio0.ssid='AltheaHome-5'
wireless.default_radio0.encryption='psk2'
wireless.default_radio0.key='ChangeMe'
wireless.radio1=wifi-device
wireless.radio1.type='mac80211'
wireless.radio1.path='platform/soc/a000000.wifi'
wireless.radio1.channel='1'
wireless.radio1.band='2g'
wireless.radio1.htmode='HT20'
wireless.radio1.disabled='0'
wireless.default_radio1=wifi-iface
wireless.default_radio1.device='radio1'
wireless.default_radio1.network='lan'
wireless.default_radio1.mode='ap'
wireless.default_radio1.ssid='AltheaHome-2.4'
wireless.default_radio1.encryption='psk2'
wireless.default_radio1.key='ChangeMe'
wireless.radio2=wifi-device
wireless.radio2.type='mac80211'
wireless.radio2.path='platform/soc/a800000.wifi'
wireless.radio2.channel='36'
wireless.radio2.band='5g'
wireless.radio2.htmode='VHT80'
wireless.radio2.disabled='0'
wireless.default_radio2=wifi-iface
wireless.default_radio2.device='radio2'
wireless.default_radio2.network='lan'
wireless.default_radio2.mode='ap'
wireless.default_radio2.ssid='AltheaHome-5'
wireless.default_radio2.encryption='psk2'
wireless.default_radio2.key='ChangeMe'
";
let res1 = extract_iw_ifnames(iw_dev_output);
assert!(res1.len() == 3);
assert_eq!(res1.get("phy1-ap0"), Some(&"1".to_string()));
assert_eq!(res1.get("phy2-ap0"), Some(&"36".to_string()));
assert_eq!(res1.get("phy0-ap0"), Some(&"149".to_string()));

let res2 = extract_uci_ifnames(uci_dev_output);
assert!(res2.len() == 3);
assert_eq!(res2.get("radio0"), Some(&"149".to_string()));
assert_eq!(res2.get("radio1"), Some(&"1".to_string()));
assert_eq!(res2.get("radio2"), Some(&"36".to_string()));

let res3 = pair_iw_and_uci(&res1, &res2);
assert!(res3.len() == 3);
assert_eq!(res3.get("phy1-ap0"), Some(&"radio1".to_string()));
assert_eq!(res3.get("phy2-ap0"), Some(&"radio2".to_string()));
assert_eq!(res3.get("phy0-ap0"), Some(&"radio0".to_string()));
}
}

0 comments on commit 6d62c60

Please sign in to comment.