fix(openwrt): detect radios and scan networks on vendor MediaTek drivers

Routers running MediaTek's proprietary mt_wifi SDK driver (e.g. GL.iNet)
never register with cfg80211/mac80211, so they have no `iw dev` entry and
no /sys/class/ieee80211 phy even though the radio is real and working —
find_wireless_iface was bailing with "No wireless radio found" on these.
Fall back to iwinfo's device listing, which abstracts over vendor backends
too, and to the vendor's iwpriv site-survey ioctl for scanning when iwinfo
itself can't trigger a scan on the interface.
This commit is contained in:
ssmithx 2026-07-01 11:59:28 +00:00
parent 6299e91544
commit 1866c40edf

View File

@ -12,13 +12,57 @@ pub struct ScannedNetwork {
pub fn scan_networks(router: &Router) -> Result<Vec<ScannedNetwork>> {
let (iface, temp) = find_wireless_iface(router)?;
let output = router.run_ok(&format!("iwinfo {} scan 2>&1", iface))?;
let result = if output.contains("Scanning not possible") {
// Vendor MediaTek `mt_wifi` driver (see find_wireless_iface) doesn't
// support scanning through iwinfo/nl80211 at all. Fall back to its own
// private ioctl site-survey, which works on the same interface.
scan_via_mtk_site_survey(router, &iface)
} else if output.contains("No scan results") || output.trim().is_empty() {
Ok(vec![])
} else {
parse_iwinfo_scan(&output)
};
if temp {
let _ = router.run(&format!("iw dev {} del 2>/dev/null", iface));
}
if output.contains("No scan results") || output.trim().is_empty() {
return Ok(vec![]);
result
}
fn scan_via_mtk_site_survey(router: &Router, iface: &str) -> Result<Vec<ScannedNetwork>> {
let _ = router.run(&format!("iwpriv {} set SiteSurvey=1 2>/dev/null", iface));
std::thread::sleep(std::time::Duration::from_secs(4));
let output = router.run_ok(&format!("iwpriv {} get_site_survey 2>&1", iface))?;
parse_mtk_site_survey(&output)
}
/// Parses MediaTek's `iwpriv <iface> get_site_survey` fixed-width table.
/// Column offsets come from the header row layout, which is part of the
/// vendor SDK's ioctl response format shared across OEMs (GL.iNet, etc.),
/// not something set per-device.
fn parse_mtk_site_survey(output: &str) -> Result<Vec<ScannedNetwork>> {
let mut networks = Vec::new();
for line in output.lines() {
if !line.trim_start().as_bytes().first().is_some_and(u8::is_ascii_digit) {
continue; // skip header/summary lines; data rows start with an index
}
let ssid = line.get(8..41).unwrap_or("").trim().to_string();
if ssid.is_empty() {
continue;
}
let bssid = line.get(41..61).unwrap_or("").trim().to_string();
let security = line.get(61..84).unwrap_or("");
let channel: u8 = line.get(4..8).and_then(|s| s.trim().parse().ok()).unwrap_or(0);
let signal: i32 = line.get(84..92).and_then(|s| s.trim().parse().ok()).unwrap_or(-100);
networks.push(ScannedNetwork {
ssid,
bssid,
signal,
channel,
encryption: normalize_encryption(security),
});
}
parse_iwinfo_scan(&output)
networks.sort_by(|a, b| b.signal.cmp(&a.signal));
Ok(networks)
}
/// Returns `(interface_name, is_temporary)`.
@ -31,6 +75,17 @@ fn find_wireless_iface(router: &Router) -> Result<(String, bool)> {
return Ok((out.trim().to_string(), false));
}
// Some vendor wifi drivers (e.g. MediaTek's out-of-tree `mt_wifi`/`mtk` SDK
// driver used by GL.iNet and others) never register with cfg80211/mac80211,
// so they have no `iw dev` entry and no /sys/class/ieee80211 phy even though
// the radio is real and already up. `iwinfo` abstracts over those vendor
// backends too, so fall back to its device listing before concluding there's
// no radio at all.
let (iwinfo_out, _) = router.run("iwinfo 2>/dev/null | awk '/^[A-Za-z]/{print $1; exit}'")?;
if !iwinfo_out.trim().is_empty() {
return Ok((iwinfo_out.trim().to_string(), false));
}
// Find the phy — if this is empty the device has no WiFi hardware at all
let (phy_out, _) = router.run("ls /sys/class/ieee80211/ 2>/dev/null | head -1")?;
let phy = phy_out.trim().to_string();