use std::path::{Path, PathBuf};
use crate::wfb_tables::{DENY_DRIVER_PREFIXES, DENY_VID, WFB_COMPATIBLE, WFB_COMPATIBLE_DRIVERS};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WirelessIface {
pub name: String,
pub driver: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApIfaceError {
NoOnboardWifi,
ConfiguredIsRadio { iface: String, driver: String },
ConfiguredMissing { iface: String },
}
impl std::fmt::Display for ApIfaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoOnboardWifi => write!(
f,
"no onboard WiFi interface found; refusing to run the access \
point on the WFB radio"
),
Self::ConfiguredIsRadio { iface, driver } => write!(
f,
"configured AP interface {iface} is the WFB radio (driver \
{driver}); refusing to take the aircraft's radio link"
),
Self::ConfiguredMissing { iface } => {
write!(f, "configured AP interface {iface} is not present")
}
}
}
}
pub fn is_injection_driver(driver: &str) -> bool {
let d = driver.trim().to_ascii_lowercase();
WFB_COMPATIBLE_DRIVERS.iter().any(|k| *k == d)
}
pub fn is_wfb_adapter(driver: &str, vid_pid: Option<(u16, u16)>) -> bool {
let d = driver.trim().to_ascii_lowercase();
if DENY_DRIVER_PREFIXES.iter().any(|p| d.starts_with(p)) {
return false;
}
if let Some((vid, pid)) = vid_pid {
if DENY_VID.contains(&vid) {
return false;
}
if WFB_COMPATIBLE
.iter()
.any(|(v, p, _)| *v == vid && *p == pid)
{
return true;
}
}
is_injection_driver(&d)
}
pub fn usb_vid_pid_in(root: &Path, iface: &str) -> Option<(u16, u16)> {
const MAX_PARENT_HOPS: usize = 4;
let mut dir = std::fs::canonicalize(root.join(iface).join("device")).ok()?;
for _ in 0..=MAX_PARENT_HOPS {
if let (Ok(vid), Ok(pid)) = (
std::fs::read_to_string(dir.join("idVendor")),
std::fs::read_to_string(dir.join("idProduct")),
) {
return Some((
u16::from_str_radix(vid.trim(), 16).ok()?,
u16::from_str_radix(pid.trim(), 16).ok()?,
));
}
dir = dir.parent()?.to_path_buf();
}
None
}
pub fn usb_vid_pid(iface: &str) -> Option<(u16, u16)> {
usb_vid_pid_in(Path::new(NET_DIR), iface)
}
pub fn is_onboard_wifi_driver(driver: &str) -> bool {
let d = driver.trim().to_ascii_lowercase();
DENY_DRIVER_PREFIXES.iter().any(|p| d.starts_with(p))
}
pub fn choose_ap_interface(
ifaces: &[WirelessIface],
configured: &str,
radio_iface: Option<&str>,
) -> Result<String, ApIfaceError> {
let is_radio = |c: &WirelessIface| {
is_injection_driver(&c.driver) || radio_iface.is_some_and(|r| r == c.name)
};
let configured = configured.trim();
if !configured.is_empty() {
return match ifaces.iter().find(|c| c.name == configured) {
Some(c) if is_radio(c) => Err(ApIfaceError::ConfiguredIsRadio {
iface: c.name.clone(),
driver: c.driver.clone(),
}),
Some(c) => Ok(c.name.clone()),
None => Err(ApIfaceError::ConfiguredMissing {
iface: configured.to_string(),
}),
};
}
ifaces
.iter()
.find(|c| is_onboard_wifi_driver(&c.driver) && !is_radio(c))
.map(|c| c.name.clone())
.ok_or(ApIfaceError::NoOnboardWifi)
}
const NET_DIR: &str = "/sys/class/net";
pub fn driver_name_in(root: &Path, iface: &str) -> Option<String> {
let link = root.join(iface).join("device").join("driver");
let target = std::fs::read_link(&link).ok()?;
Some(target.file_name()?.to_string_lossy().into_owned())
}
pub fn is_wireless_in(root: &Path, iface: &str) -> bool {
root.join(iface).join("phy80211").exists() || root.join(iface).join("wireless").exists()
}
pub fn list_wireless_in(root: &Path) -> Vec<WirelessIface> {
let mut out: Vec<WirelessIface> = std::fs::read_dir(root)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
if name == "lo" || !is_wireless_in(root, &name) {
return None;
}
Some(WirelessIface {
driver: driver_name_in(root, &name)?,
name,
})
})
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub fn list_wireless() -> Vec<WirelessIface> {
list_wireless_in(&PathBuf::from(NET_DIR))
}
pub const WFB_STATS_PATH: &str = "/run/ados/wfb-stats.json";
pub fn radio_interface_from(path: &Path) -> Option<String> {
let txt = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&txt).ok()?;
let iface = v.get("interface")?.as_str()?.trim().to_string();
(!iface.is_empty()).then_some(iface)
}
pub fn radio_interface() -> Option<String> {
radio_interface_from(Path::new(WFB_STATS_PATH))
}
pub fn resolve_ap_interface(
configured: &str,
radio_iface: Option<&str>,
) -> Result<String, ApIfaceError> {
choose_ap_interface(&list_wireless(), configured, radio_iface)
}
const NOT_WIRED_DRIVERS: &[&str] = &[
"g_ether",
"g_multi",
"rndis_host",
"ipheth",
"qmi_wwan",
"cdc_mbim",
"huawei_cdc_ncm",
];
pub fn is_wired_in(root: &Path, iface: &str) -> bool {
if iface == "lo" || is_wireless_in(root, iface) {
return false;
}
let dir = root.join(iface);
let ether = std::fs::read_to_string(dir.join("type"))
.map(|t| t.trim() == "1")
.unwrap_or(false);
if !ether {
return false;
}
let device = match std::fs::read_link(dir.join("device")) {
Ok(target) => target,
Err(_) => return false,
};
if device.to_string_lossy().contains("gadget") {
return false;
}
match driver_name_in(root, iface) {
Some(driver) => {
!NOT_WIRED_DRIVERS.contains(&driver.as_str()) && !driver.starts_with("usb_f_")
}
None => false,
}
}
pub fn list_wired_in(root: &Path) -> Vec<String> {
let mut out: Vec<String> = std::fs::read_dir(root)
.into_iter()
.flatten()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|name| is_wired_in(root, name))
.collect();
out.sort();
out
}
pub fn list_wired() -> Vec<String> {
list_wired_in(&PathBuf::from(NET_DIR))
}
pub fn mesh_capable_in(root: &Path) -> bool {
list_wireless_in(root)
.iter()
.filter(|w| is_wfb_adapter(&w.driver, usb_vid_pid_in(root, &w.name)))
.count()
>= 2
}
pub fn mesh_capable() -> bool {
mesh_capable_in(Path::new(NET_DIR))
}
const CELLULAR_DRIVERS: &[&str] = &[
"qmi_wwan",
"cdc_mbim",
"huawei_cdc_ncm",
"cdc_ncm",
"rndis_host",
];
const MODEM_USB_VIDS: &[u16] = &[0x1199, 0x2c7c, 0x12d1, 0x05c6, 0x1e0e, 0x2cb7, 0x1bc7];
pub fn is_cellular_in(root: &Path, iface: &str) -> bool {
let Ok(device) = std::fs::read_link(root.join(iface).join("device")) else {
return false;
};
let path = device.to_string_lossy();
if path.contains("gadget") || path.contains("udc") {
return false;
}
match driver_name_in(root, iface).as_deref() {
Some(d) if CELLULAR_DRIVERS.contains(&d) => true,
Some("option") => {
usb_vid_pid_in(root, iface).is_some_and(|(vid, _)| MODEM_USB_VIDS.contains(&vid))
}
_ => false,
}
}
pub fn list_cellular_in(root: &Path) -> Vec<String> {
let mut out: Vec<String> = std::fs::read_dir(root)
.into_iter()
.flatten()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|name| is_cellular_in(root, name))
.collect();
out.sort();
out
}
pub fn list_cellular() -> Vec<String> {
list_cellular_in(&PathBuf::from(NET_DIR))
}
#[cfg(test)]
#[path = "netif_tests.rs"]
mod tests;