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,
|
'scanning': scanning, 'scan_remaining': remaining, 'stale': stale,
|
||||||
'hopper_online': _hopper_online(),
|
'hopper_online': _hopper_online(),
|
||||||
'wlan0_pinned': _wlan0_pinned(),
|
'wlan0_pinned': _wlan0_pinned(),
|
||||||
|
'wlan0_sta': _sta_uplink_enabled(),
|
||||||
'wlan0_hopping': _wlan0_hopping(),
|
'wlan0_hopping': _wlan0_hopping(),
|
||||||
'history_reset': _recon_history_reset()}
|
'history_reset': _recon_history_reset()}
|
||||||
|
|
||||||
@@ -3717,6 +3718,20 @@ def _wlan0_pinned():
|
|||||||
return False
|
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():
|
def _pineap_interfaces():
|
||||||
"""Runtime pineapd interface table from ``_pineap INTERFACE LIST``:
|
"""Runtime pineapd interface table from ``_pineap INTERFACE LIST``:
|
||||||
{name: {'channels': int, 'bands': str, 'hop': str, 'pkts': int}}."""
|
{name: {'channels': int, 'bands': str, 'hop': str, 'pkts': int}}."""
|
||||||
@@ -3790,6 +3805,13 @@ def env_check():
|
|||||||
else:
|
else:
|
||||||
_env_step(report, 'warn', pool_detail)
|
_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()
|
raised = _raise_monitors()
|
||||||
if raised:
|
if raised:
|
||||||
_env_step(report, 'fixed', 'monitor interfaces brought up', ', '.join(raised))
|
_env_step(report, 'fixed', 'monitor interfaces brought up', ', '.join(raised))
|
||||||
|
|||||||
@@ -1462,7 +1462,7 @@ views.recon = (root) => {
|
|||||||
compare: [], history: {}, mapBand: null,
|
compare: [], history: {}, mapBand: null,
|
||||||
archive: null, archives: [], scanRemaining: null,
|
archive: null, archives: [], scanRemaining: null,
|
||||||
hopperOnline: null, historyReset: false, scanErr: null,
|
hopperOnline: null, historyReset: false, scanErr: null,
|
||||||
wlan0Pinned: false, wlan0Hopping: null };
|
wlan0Pinned: false, wlan0Sta: false, wlan0Hopping: null };
|
||||||
const cols = reconLoadCols();
|
const cols = reconLoadCols();
|
||||||
|
|
||||||
// ---- title cards (stat cards with optional mini charts) ----
|
// ---- 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.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.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.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.wlan0Hopping === false) bits.push('2.4GHz starved — wlan0mon hopping is off');
|
||||||
if (state.scanErr) bits.push(state.scanErr);
|
if (state.scanErr) bits.push(state.scanErr);
|
||||||
scanStatus.textContent = bits.join(' · ');
|
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', () => {
|
scanToggle.addEventListener('change', () => {
|
||||||
if (pendingScan) { scanToggle.checked = !scanToggle.checked; return; }
|
if (pendingScan) { scanToggle.checked = !scanToggle.checked; return; }
|
||||||
@@ -2527,6 +2528,7 @@ views.recon = (root) => {
|
|||||||
state.hopperOnline = r.data.hopper_online;
|
state.hopperOnline = r.data.hopper_online;
|
||||||
state.historyReset = !!r.data.history_reset;
|
state.historyReset = !!r.data.history_reset;
|
||||||
state.wlan0Pinned = !!r.data.wlan0_pinned;
|
state.wlan0Pinned = !!r.data.wlan0_pinned;
|
||||||
|
state.wlan0Sta = !!r.data.wlan0_sta;
|
||||||
state.wlan0Hopping = r.data.wlan0_hopping;
|
state.wlan0Hopping = r.data.wlan0_hopping;
|
||||||
if (!pendingScan) scanToggle.checked = scanning;
|
if (!pendingScan) scanToggle.checked = scanning;
|
||||||
renderScanBar();
|
renderScanBar();
|
||||||
|
|||||||
@@ -217,6 +217,35 @@ class EnvCheckTest(unittest.TestCase):
|
|||||||
report = server.env_check()
|
report = server.env_check()
|
||||||
self.assertEqual(self.steps(report, 'wlan0mon hopping on')[0]['ok'], 'pass')
|
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):
|
def test_cli_exits_zero_on_pass(self):
|
||||||
self.safe_set()
|
self.safe_set()
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
|
|||||||
Reference in New Issue
Block a user