fix: recon env-check verifies runtime wlan0mon hopping (2.4GHz starvation)
The band UCI config looked correct but pineapd was not hopping wlan0mon at runtime, so 2.4GHz recon results were entirely absent. env_check now parses _pineap INTERFACE LIST and warns when wlan0mon hop is off; /api/recon/status exposes wlan0_hopping and the recon scan bar surfaces it.
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_hopping': _wlan0_hopping(),
|
||||||
'history_reset': _recon_history_reset()}
|
'history_reset': _recon_history_reset()}
|
||||||
|
|
||||||
|
|
||||||
@@ -3716,6 +3717,40 @@ def _wlan0_pinned():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _pineap_interfaces():
|
||||||
|
"""Runtime pineapd interface table from ``_pineap INTERFACE LIST``:
|
||||||
|
{name: {'channels': int, 'bands': str, 'hop': str, 'pkts': int}}."""
|
||||||
|
rc, out, err = _pineap('INTERFACE', 'LIST', timeout=15)
|
||||||
|
if rc != 0:
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
for line in (out or '').splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 7 or not parts[0].startswith('wlan'):
|
||||||
|
continue
|
||||||
|
result[parts[0]] = {
|
||||||
|
'channels': int(parts[1]) if parts[1].isdigit() else None,
|
||||||
|
'bands': parts[2],
|
||||||
|
'type': parts[3],
|
||||||
|
'hop': parts[4],
|
||||||
|
'chan': parts[5],
|
||||||
|
'pkts': int(parts[6]) if parts[6].isdigit() else None,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _wlan0_hopping():
|
||||||
|
"""Whether pineapd is actually hopping wlan0mon at runtime. The band UCI
|
||||||
|
config can be correct while runtime hopping is off, which starves 2.4GHz
|
||||||
|
recon results entirely."""
|
||||||
|
ifaces = _pineap_interfaces()
|
||||||
|
mon = ifaces.get('wlan0mon')
|
||||||
|
if not mon:
|
||||||
|
return None
|
||||||
|
hop = mon.get('hop')
|
||||||
|
return bool(hop) and hop not in ('0', '', 'none', 'false')
|
||||||
|
|
||||||
|
|
||||||
def env_check():
|
def env_check():
|
||||||
"""Full environment pass, run at service startup and via
|
"""Full environment pass, run at service startup and via
|
||||||
``server.py --env-check``. Auto-fixes everything fixable, re-verifies, and
|
``server.py --env-check``. Auto-fixes everything fixable, re-verifies, and
|
||||||
@@ -3774,6 +3809,16 @@ def env_check():
|
|||||||
else:
|
else:
|
||||||
_env_step(report, 'pass', 'no radio0 AP pins wlan0mon')
|
_env_step(report, 'pass', 'no radio0 AP pins wlan0mon')
|
||||||
|
|
||||||
|
hopping = _wlan0_hopping()
|
||||||
|
if hopping is False:
|
||||||
|
_env_step(report, 'warn', '2.4GHz recon starved: wlan0mon hopping is off at '
|
||||||
|
'runtime (INTERFACE LIST hop=0)')
|
||||||
|
elif hopping is None:
|
||||||
|
_env_step(report, 'warn', 'could not read pineapd interface state '
|
||||||
|
'(INTERFACE LIST failed)')
|
||||||
|
else:
|
||||||
|
_env_step(report, 'pass', 'wlan0mon hopping on (2.4GHz scanning active)')
|
||||||
|
|
||||||
ENV_CHECK_STATE['report'] = report
|
ENV_CHECK_STATE['report'] = report
|
||||||
ENV_CHECK_STATE['overall'] = _env_overall(report)
|
ENV_CHECK_STATE['overall'] = _env_overall(report)
|
||||||
ENV_CHECK_STATE['updated'] = time.time()
|
ENV_CHECK_STATE['updated'] = time.time()
|
||||||
|
|||||||
@@ -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 };
|
wlan0Pinned: 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,9 +1661,10 @@ 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.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.scanErr);
|
scanStatus.classList.toggle('warn', state.hopperOnline === false || state.historyReset || state.wlan0Pinned || 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; }
|
||||||
@@ -2526,6 +2527,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.wlan0Hopping = r.data.wlan0_hopping;
|
||||||
if (!pendingScan) scanToggle.checked = scanning;
|
if (!pendingScan) scanToggle.checked = scanning;
|
||||||
renderScanBar();
|
renderScanBar();
|
||||||
if (wasScanning !== scanning) restartPoll();
|
if (wasScanning !== scanning) restartPoll();
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class EnvCheckTest(unittest.TestCase):
|
|||||||
self.daemon_ok = True
|
self.daemon_ok = True
|
||||||
self.iface_up = {'wlan0mon': True, 'wlan1mon': True}
|
self.iface_up = {'wlan0mon': True, 'wlan1mon': True}
|
||||||
self.uci_state = {}
|
self.uci_state = {}
|
||||||
|
self.interface_list = (
|
||||||
|
'Interface #ch Bands Type Hop Chan Pkts \n'
|
||||||
|
'wlan1mon 25 5 max fast hop 21567\n'
|
||||||
|
'wlan0mon 11 2 max fast hop 452\n')
|
||||||
server.ENV_CHECK_STATE.update({'report': None, 'overall': None, 'updated': 0,
|
server.ENV_CHECK_STATE.update({'report': None, 'overall': None, 'updated': 0,
|
||||||
'pool_runtime': None})
|
'pool_runtime': None})
|
||||||
self.old_iface_up = server._iface_up
|
self.old_iface_up = server._iface_up
|
||||||
@@ -77,6 +81,8 @@ class EnvCheckTest(unittest.TestCase):
|
|||||||
return (0, ''.join("%s=%s\n" % (k, v) for k, v in self.uci_state.items()
|
return (0, ''.join("%s=%s\n" % (k, v) for k, v in self.uci_state.items()
|
||||||
if k.startswith(sec + '.')), '')
|
if k.startswith(sec + '.')), '')
|
||||||
if a[0] == '_pineap':
|
if a[0] == '_pineap':
|
||||||
|
if len(a) > 1 and a[1] == 'INTERFACE':
|
||||||
|
return (0, self.interface_list, '')
|
||||||
return (0, '', '')
|
return (0, '', '')
|
||||||
if a[0] in ('ip', '/etc/init.d/pineapd'):
|
if a[0] in ('ip', '/etc/init.d/pineapd'):
|
||||||
return (0, '', '')
|
return (0, '', '')
|
||||||
@@ -191,6 +197,26 @@ class EnvCheckTest(unittest.TestCase):
|
|||||||
report = server.env_check()
|
report = server.env_check()
|
||||||
self.assertEqual(self.steps(report, 'no radio0 AP pins wlan0mon')[0]['ok'], 'pass')
|
self.assertEqual(self.steps(report, 'no radio0 AP pins wlan0mon')[0]['ok'], 'pass')
|
||||||
|
|
||||||
|
def test_wlan0_starved_warns(self):
|
||||||
|
self.safe_set()
|
||||||
|
self.interface_list = (
|
||||||
|
'Interface #ch Bands Type Hop Chan Pkts \n'
|
||||||
|
'wlan1mon 25 5 max fast hop 21567\n'
|
||||||
|
'wlan0mon 11 2 max 0 hop 452\n')
|
||||||
|
report = server.env_check()
|
||||||
|
self.assertEqual(self.steps(report, 'wlan0mon hopping is off')[0]['ok'], 'warn')
|
||||||
|
|
||||||
|
def test_wlan0_hopping_unknown_warns(self):
|
||||||
|
self.safe_set()
|
||||||
|
self.interface_list = ''
|
||||||
|
report = server.env_check()
|
||||||
|
self.assertEqual(self.steps(report, 'could not read pineapd interface state')[0]['ok'], 'warn')
|
||||||
|
|
||||||
|
def test_wlan0_hopping_pass(self):
|
||||||
|
self.safe_set()
|
||||||
|
report = server.env_check()
|
||||||
|
self.assertEqual(self.steps(report, 'wlan0mon hopping on')[0]['ok'], 'pass')
|
||||||
|
|
||||||
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