Improve WiFi quick scan error handling and Linux tool fallback

- Add fallback mechanism to try multiple tools (nmcli -> iw -> iwlist)
- Improve error messages for iw/iwlist with root privilege detection
- Enhance nmcli scanner to try without interface if specific scan fails
- Better error reporting in frontend showing actual backend errors
- Add logging throughout scan process for debugging

This fixes quick scan immediately failing on Linux systems by trying
multiple tools and providing meaningful error messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-21 22:57:51 +00:00
parent dfd4b0e89e
commit f665203543
2 changed files with 137 additions and 47 deletions
+22 -8
View File
@@ -302,18 +302,32 @@ const WiFiMode = (function() {
const result = await response.json();
console.log('[WiFiMode] Quick scan complete:', result);
// Check if we got results
if (!result.access_points || result.access_points.length === 0) {
if (result.error) {
throw new Error(result.error);
}
// No error but no results - might need different tool
console.warn('[WiFiMode] Quick scan returned no networks. Try Deep Scan instead.');
showError('Quick scan found no networks. System tools may not be available. Try Deep Scan with monitor mode.');
// Check for error first
if (result.error) {
console.error('[WiFiMode] Quick scan error from server:', result.error);
showError(result.error);
setScanning(false);
return;
}
// Check if we got results
if (!result.access_points || result.access_points.length === 0) {
// No error but no results
let msg = 'Quick scan found no networks in range.';
if (result.warnings && result.warnings.length > 0) {
msg += ' Warnings: ' + result.warnings.join('; ');
}
console.warn('[WiFiMode] ' + msg);
showError(msg + ' Try Deep Scan with monitor mode.');
setScanning(false);
return;
}
// Show any warnings even on success
if (result.warnings && result.warnings.length > 0) {
console.warn('[WiFiMode] Quick scan warnings:', result.warnings);
}
// Process results
processQuickScanResult(result);
+115 -39
View File
@@ -300,29 +300,54 @@ class UnifiedWiFiScanner:
result.interface = iface
# Select and run parser based on platform/tools
# Try multiple tools with fallback on Linux
observations = []
tool_used = None
errors_encountered = []
try:
if self._capabilities.platform == 'darwin':
if self._capabilities.has_airport:
observations = self._scan_with_airport(iface, timeout)
tool_used = 'airport'
else: # Linux
else:
result.error = "No WiFi scanning tool available on macOS (airport not found)"
result.is_complete = True
return result
else: # Linux - try tools in order with fallback
tools_to_try = []
if self._capabilities.has_nmcli:
observations = self._scan_with_nmcli(iface, timeout)
tool_used = 'nmcli'
elif self._capabilities.has_iw:
observations = self._scan_with_iw(iface, timeout)
tool_used = 'iw'
elif self._capabilities.has_iwlist:
observations = self._scan_with_iwlist(iface, timeout)
tool_used = 'iwlist'
tools_to_try.append(('nmcli', self._scan_with_nmcli))
if self._capabilities.has_iw:
tools_to_try.append(('iw', self._scan_with_iw))
if self._capabilities.has_iwlist:
tools_to_try.append(('iwlist', self._scan_with_iwlist))
if not tool_used:
result.error = "No WiFi scanning tool available"
result.is_complete = True
return result
if not tools_to_try:
result.error = "No WiFi scanning tools available. Install NetworkManager (nmcli) or wireless-tools (iw/iwlist)."
result.is_complete = True
return result
for tool_name, scan_func in tools_to_try:
try:
logger.info(f"Attempting quick scan with {tool_name} on {iface}")
observations = scan_func(iface, timeout)
tool_used = tool_name
logger.info(f"Quick scan with {tool_name} found {len(observations)} networks")
break # Success, stop trying other tools
except Exception as e:
error_msg = f"{tool_name}: {str(e)}"
errors_encountered.append(error_msg)
logger.warning(f"Quick scan with {tool_name} failed: {e}")
continue # Try next tool
if not tool_used:
# All tools failed
result.error = "All scan tools failed. " + "; ".join(errors_encountered)
if not self._capabilities.is_root:
result.error += " (Note: iw/iwlist require root privileges)"
result.is_complete = True
return result
# Process observations into access points
for obs in observations:
@@ -332,10 +357,16 @@ class UnifiedWiFiScanner:
with self._lock:
result.access_points = list(self._access_points.values())
# Add warnings for tools that failed before one succeeded
for err in errors_encountered:
result.warnings.append(err)
# Generate channel stats
result.channel_stats = self._calculate_channel_stats()
result.recommendations = self._generate_recommendations(result.channel_stats)
logger.info(f"Quick scan complete: {len(result.access_points)} networks found using {tool_used}")
except subprocess.TimeoutExpired:
result.error = f"Scan timed out after {timeout}s"
result.warnings.append(f"Tool '{tool_used}' timed out")
@@ -383,14 +414,21 @@ class UnifiedWiFiScanner:
from .parsers.nmcli import parse_nmcli_scan
try:
# Trigger a rescan first
subprocess.run(
# Try to trigger a rescan first (might fail if interface not managed by NM)
rescan_result = subprocess.run(
['nmcli', 'device', 'wifi', 'rescan', 'ifname', interface],
capture_output=True,
timeout=timeout / 2,
)
if rescan_result.returncode != 0:
# Try without interface specification
subprocess.run(
['nmcli', 'device', 'wifi', 'rescan'],
capture_output=True,
timeout=timeout / 2,
)
# Get results
# Get results - try with interface first, then without
result = subprocess.run(
['nmcli', '-t', '-f', 'BSSID,SSID,MODE,CHAN,FREQ,RATE,SIGNAL,SECURITY', 'device', 'wifi', 'list', 'ifname', interface],
capture_output=True,
@@ -398,10 +436,28 @@ class UnifiedWiFiScanner:
timeout=timeout,
)
# If interface-specific scan failed, try general scan
if result.returncode != 0 or not result.stdout.strip():
logger.debug(f"nmcli scan with interface {interface} failed, trying general scan")
result = subprocess.run(
['nmcli', '-t', '-f', 'BSSID,SSID,MODE,CHAN,FREQ,RATE,SIGNAL,SECURITY', 'device', 'wifi', 'list'],
capture_output=True,
text=True,
timeout=timeout,
)
if result.returncode != 0:
error_msg = result.stderr.strip() or f"nmcli returned code {result.returncode}"
logger.warning(f"nmcli scan failed: {error_msg}")
raise RuntimeError(f"nmcli scan failed: {error_msg}")
# Check for common issues
if 'not running' in error_msg.lower():
raise RuntimeError("NetworkManager is not running")
elif 'not found' in error_msg.lower() or 'no such' in error_msg.lower():
raise RuntimeError(f"Interface {interface} not found or not managed by NetworkManager")
else:
raise RuntimeError(f"nmcli scan failed: {error_msg}")
if not result.stdout.strip():
raise RuntimeError("nmcli returned no results (WiFi might be disabled or no networks in range)")
return parse_nmcli_scan(result.stdout)
except subprocess.TimeoutExpired:
@@ -413,36 +469,56 @@ class UnifiedWiFiScanner:
"""Scan using iw."""
from .parsers.iw import parse_iw_scan
result = subprocess.run(
['iw', interface, 'scan'],
capture_output=True,
text=True,
timeout=timeout,
)
try:
result = subprocess.run(
['iw', interface, 'scan'],
capture_output=True,
text=True,
timeout=timeout,
)
if result.returncode != 0:
# May need root
logger.warning(f"iw scan failed: {result.stderr}")
return []
if result.returncode != 0:
error_msg = result.stderr.strip() or f"iw returned code {result.returncode}"
# Check for common errors
if 'Operation not permitted' in error_msg or 'Permission denied' in error_msg:
raise RuntimeError(f"iw scan requires root privileges: {error_msg}")
elif 'Network is down' in error_msg:
raise RuntimeError(f"Interface {interface} is down: {error_msg}")
else:
raise RuntimeError(f"iw scan failed: {error_msg}")
return parse_iw_scan(result.stdout)
return parse_iw_scan(result.stdout)
except subprocess.TimeoutExpired:
raise RuntimeError(f"iw scan timed out after {timeout}s")
except FileNotFoundError:
raise RuntimeError("iw not found (wireless-tools not installed)")
def _scan_with_iwlist(self, interface: str, timeout: float) -> list[WiFiObservation]:
"""Scan using iwlist."""
from .parsers.iwlist import parse_iwlist_scan
result = subprocess.run(
['iwlist', interface, 'scan'],
capture_output=True,
text=True,
timeout=timeout,
)
try:
result = subprocess.run(
['iwlist', interface, 'scan'],
capture_output=True,
text=True,
timeout=timeout,
)
if result.returncode != 0:
logger.warning(f"iwlist scan failed: {result.stderr}")
return []
if result.returncode != 0:
error_msg = result.stderr.strip() or f"iwlist returned code {result.returncode}"
if 'Operation not permitted' in error_msg or 'Permission denied' in error_msg:
raise RuntimeError(f"iwlist scan requires root privileges: {error_msg}")
elif 'Network is down' in error_msg:
raise RuntimeError(f"Interface {interface} is down: {error_msg}")
else:
raise RuntimeError(f"iwlist scan failed: {error_msg}")
return parse_iwlist_scan(result.stdout)
return parse_iwlist_scan(result.stdout)
except subprocess.TimeoutExpired:
raise RuntimeError(f"iwlist scan timed out after {timeout}s")
except FileNotFoundError:
raise RuntimeError("iwlist not found (wireless-tools not installed)")
# =========================================================================
# Deep Scan (airodump-ng)