From 5ab569f1501a991235b892f9e820aba92aa88ca7 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Mon, 29 Jun 2026 16:39:11 +0000 Subject: [PATCH] fix(openwrt): use iw phy interface add for scan when no UCI wifi-iface exists wifi up does nothing without a wifi-iface section in UCI (common on fresh flash). Instead, create a temporary managed interface directly on phy0 via nl80211 (iw phy phy0 interface add scan0 type managed), scan on it, then delete it. No netifd/UCI involvement needed for scanning. Co-Authored-By: Claude Sonnet 4.6 --- core/openwrt/src/wifi_scan.rs | 44 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/core/openwrt/src/wifi_scan.rs b/core/openwrt/src/wifi_scan.rs index 882b9715..683da51b 100644 --- a/core/openwrt/src/wifi_scan.rs +++ b/core/openwrt/src/wifi_scan.rs @@ -10,42 +10,46 @@ pub struct ScannedNetwork { } pub fn scan_networks(router: &Router) -> Result> { - let iface = find_wireless_iface(router)?; + let (iface, temp) = find_wireless_iface(router)?; let output = router.run_ok(&format!("iwinfo {} scan 2>&1", iface))?; + 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![]); } parse_iwinfo_scan(&output) } -fn find_wireless_iface(router: &Router) -> Result { - // Fast path: interface already up (radio was previously enabled) +/// Returns `(interface_name, is_temporary)`. +/// If no interface exists, creates a temporary managed one directly on the PHY +/// so we can scan without needing any UCI wifi-iface sections. +fn find_wireless_iface(router: &Router) -> Result<(String, bool)> { + // Fast path: an interface already exists (radio was enabled previously) let (out, _) = router.run("iw dev 2>/dev/null | awk '/Interface/{print $2}' | head -1")?; if !out.trim().is_empty() { - return Ok(out.trim().to_string()); + return Ok((out.trim().to_string(), false)); } - // No interface yet — common on a freshly-flashed OpenWrt where radio0 is - // disabled by default. Verify the radio PHY exists at all. + // 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")?; - if phy_out.trim().is_empty() { + let phy = phy_out.trim().to_string(); + if phy.is_empty() { anyhow::bail!("No wireless radio found on this router"); } - // Enable radio0 and bring wifi up so netifd creates the virtual interface. - tracing::info!("[{}] Radio present but no interface — enabling radio0 and running wifi up", router.host); - router.run_ok("uci set wireless.radio0.disabled=0 && uci commit wireless && wifi up 2>&1")?; + // Create a temporary managed interface directly on the PHY. This bypasses + // netifd entirely so it works even when there are no wifi-iface sections in + // UCI (common on a freshly-flashed device). + tracing::info!("[{}] Creating temporary scan interface on {}", router.host, phy); + // Remove any stale scan0 from a previous attempt, then add fresh + let _ = router.run("iw dev scan0 del 2>/dev/null"); + router.run_ok(&format!( + "iw phy {} interface add scan0 type managed 2>&1 && ip link set scan0 up 2>&1", + phy + ))?; - // Wait up to 8s for netifd to create the interface (it's asynchronous) - for _ in 0..8 { - std::thread::sleep(std::time::Duration::from_secs(1)); - let (out2, _) = router.run("iw dev 2>/dev/null | awk '/Interface/{print $2}' | head -1")?; - if !out2.trim().is_empty() { - return Ok(out2.trim().to_string()); - } - } - - anyhow::bail!("WiFi radio enabled but no interface appeared — check OpenWrt wireless config") + Ok(("scan0".to_string(), true)) } fn parse_iwinfo_scan(output: &str) -> Result> {