feat: env check auto-disables dummy_radio0 STA (2.4GHz recon root cause)
The stock STA client interface (wlan0) holds phy0's channel, pinning wlan0mon so 2.4GHz recon captures nothing (verified: iw set channel -> Resource busy until wlan0 is down). env_check now disables it (uci wireless.dummy_radio0 disabled=1 + wifi reload) at startup and /api/recon/status exposes wlan0_sta so the recon page can warn if it regresses.
This commit is contained in:
@@ -1359,6 +1359,7 @@ def h_recon_status(ctx):
|
||||
'scanning': scanning, 'scan_remaining': remaining, 'stale': stale,
|
||||
'hopper_online': _hopper_online(),
|
||||
'wlan0_pinned': _wlan0_pinned(),
|
||||
'wlan0_sta': _sta_uplink_enabled(),
|
||||
'wlan0_hopping': _wlan0_hopping(),
|
||||
'history_reset': _recon_history_reset()}
|
||||
|
||||
@@ -3717,6 +3718,20 @@ def _wlan0_pinned():
|
||||
return False
|
||||
|
||||
|
||||
def _sta_uplink_enabled():
|
||||
"""True when the stock dummy_radio0 STA client interface is enabled. The
|
||||
STA holds phy0's channel, which pins wlan0mon and starves 2.4GHz recon
|
||||
entirely (iw set channel fails with Resource busy while it is up)."""
|
||||
cfg = _uci_values('wireless.dummy_radio0') or {}
|
||||
return cfg.get('mode') == 'sta' and cfg.get('disabled') != '1'
|
||||
|
||||
|
||||
def _disable_sta_uplink():
|
||||
device_run(['uci', 'set', 'wireless.dummy_radio0.disabled=1'])
|
||||
device_run(['uci', 'commit', 'wireless'])
|
||||
device_run(['wifi', 'reload'], timeout=30)
|
||||
|
||||
|
||||
def _pineap_interfaces():
|
||||
"""Runtime pineapd interface table from ``_pineap INTERFACE LIST``:
|
||||
{name: {'channels': int, 'bands': str, 'hop': str, 'pkts': int}}."""
|
||||
@@ -3790,6 +3805,13 @@ def env_check():
|
||||
else:
|
||||
_env_step(report, 'warn', pool_detail)
|
||||
|
||||
if _sta_uplink_enabled():
|
||||
_disable_sta_uplink()
|
||||
_env_step(report, 'fixed', '2.4GHz recon: dummy_radio0 STA uplink disabled '
|
||||
'(it was pinning phy0 so wlan0mon could not hop)')
|
||||
else:
|
||||
_env_step(report, 'pass', 'no STA uplink pinning phy0')
|
||||
|
||||
raised = _raise_monitors()
|
||||
if raised:
|
||||
_env_step(report, 'fixed', 'monitor interfaces brought up', ', '.join(raised))
|
||||
|
||||
@@ -1462,7 +1462,7 @@ views.recon = (root) => {
|
||||
compare: [], history: {}, mapBand: null,
|
||||
archive: null, archives: [], scanRemaining: null,
|
||||
hopperOnline: null, historyReset: false, scanErr: null,
|
||||
wlan0Pinned: false, wlan0Hopping: null };
|
||||
wlan0Pinned: false, wlan0Sta: false, wlan0Hopping: null };
|
||||
const cols = reconLoadCols();
|
||||
|
||||
// ---- title cards (stat cards with optional mini charts) ----
|
||||
@@ -1661,10 +1661,11 @@ views.recon = (root) => {
|
||||
if (state.hopperOnline === false) bits.push('Hopper radio offline — fewer networks seen');
|
||||
if (state.historyReset) bits.push('History reset — previous scans archived (see Previous Scans)');
|
||||
if (state.wlan0Pinned) bits.push('2.4GHz under-sampled — OpenAP/Evil WPA holds wlan0mon');
|
||||
if (state.wlan0Sta) bits.push('2.4GHz starved — client STA (wlan0) pins phy0');
|
||||
if (state.wlan0Hopping === false) bits.push('2.4GHz starved — wlan0mon hopping is off');
|
||||
if (state.scanErr) bits.push(state.scanErr);
|
||||
scanStatus.textContent = bits.join(' · ');
|
||||
scanStatus.classList.toggle('warn', state.hopperOnline === false || state.historyReset || state.wlan0Pinned || state.wlan0Hopping === false || !!state.scanErr);
|
||||
scanStatus.classList.toggle('warn', state.hopperOnline === false || state.historyReset || state.wlan0Pinned || state.wlan0Sta || state.wlan0Hopping === false || !!state.scanErr);
|
||||
}
|
||||
scanToggle.addEventListener('change', () => {
|
||||
if (pendingScan) { scanToggle.checked = !scanToggle.checked; return; }
|
||||
@@ -2527,6 +2528,7 @@ views.recon = (root) => {
|
||||
state.hopperOnline = r.data.hopper_online;
|
||||
state.historyReset = !!r.data.history_reset;
|
||||
state.wlan0Pinned = !!r.data.wlan0_pinned;
|
||||
state.wlan0Sta = !!r.data.wlan0_sta;
|
||||
state.wlan0Hopping = r.data.wlan0_hopping;
|
||||
if (!pendingScan) scanToggle.checked = scanning;
|
||||
renderScanBar();
|
||||
|
||||
@@ -217,6 +217,35 @@ class EnvCheckTest(unittest.TestCase):
|
||||
report = server.env_check()
|
||||
self.assertEqual(self.steps(report, 'wlan0mon hopping on')[0]['ok'], 'pass')
|
||||
|
||||
def test_sta_uplink_disabled_when_enabled(self):
|
||||
self.safe_set()
|
||||
self.uci_state['wireless.dummy_radio0.mode'] = 'sta'
|
||||
self.uci_state['wireless.dummy_radio0.ifname'] = 'wlan0'
|
||||
self.uci_state['wireless.dummy_radio0.disabled'] = '0'
|
||||
report = server.env_check()
|
||||
self.assertEqual(self.steps(report, 'dummy_radio0 STA uplink disabled')[0]['ok'], 'fixed')
|
||||
self.assertEqual(self.uci_state['wireless.dummy_radio0.disabled'], '1')
|
||||
self.assertIn(['wifi', 'reload'], self.runs)
|
||||
|
||||
def test_sta_uplink_pass_when_absent(self):
|
||||
self.safe_set()
|
||||
report = server.env_check()
|
||||
self.assertEqual(self.steps(report, 'no STA uplink pinning phy0')[0]['ok'], 'pass')
|
||||
|
||||
def test_sta_uplink_pass_when_disabled(self):
|
||||
self.safe_set()
|
||||
self.uci_state['wireless.dummy_radio0.mode'] = 'sta'
|
||||
self.uci_state['wireless.dummy_radio0.disabled'] = '1'
|
||||
report = server.env_check()
|
||||
self.assertEqual(self.steps(report, 'no STA uplink pinning phy0')[0]['ok'], 'pass')
|
||||
|
||||
def test_recon_status_exposes_sta(self):
|
||||
self.safe_set()
|
||||
self.uci_state['wireless.dummy_radio0.mode'] = 'sta'
|
||||
status, payload = server.h_recon_status(type('C', (), {'query': {}})())
|
||||
self.assertEqual(status, 200)
|
||||
self.assertTrue(payload['wlan0_sta'])
|
||||
|
||||
def test_cli_exits_zero_on_pass(self):
|
||||
self.safe_set()
|
||||
buf = io.StringIO()
|
||||
|
||||
Reference in New Issue
Block a user