feat(reliability): PSK uplink security-mode fallback chain (sae-mixed/sae/psk2 with PMF)
This commit is contained in:
@@ -9,6 +9,10 @@ IFACE = 'wlan1up'
|
|||||||
# wifi reload returns while wpa_supplicant is still scanning/authenticating;
|
# wifi reload returns while wpa_supplicant is still scanning/authenticating;
|
||||||
# poll instead of checking once or every real uplink would false-fail.
|
# poll instead of checking once or every real uplink would false-fail.
|
||||||
ASSOC_ATTEMPTS = 5
|
ASSOC_ATTEMPTS = 5
|
||||||
|
# Security modes tried in order for PSK uplinks. sae-mixed covers
|
||||||
|
# WPA2/WPA3 transition APs; plain SAE covers WPA3-only (PMF required);
|
||||||
|
# psk2 covers legacy WPA2-PSK. ieee80211w matches each mode's PMF need.
|
||||||
|
PSK_MODE_CHAIN = (('sae-mixed', '1'), ('sae', '2'), ('psk2', '0'))
|
||||||
ASSOC_WAIT_SECONDS = 2
|
ASSOC_WAIT_SECONDS = 2
|
||||||
|
|
||||||
|
|
||||||
@@ -94,36 +98,43 @@ def set_role(role, ssid=None, psk=None):
|
|||||||
if role == 'uplink':
|
if role == 'uplink':
|
||||||
if not ssid:
|
if not ssid:
|
||||||
return {'ok': False, 'error': 'ssid required'}
|
return {'ok': False, 'error': 'ssid required'}
|
||||||
cmds = [
|
base_cmds = [
|
||||||
['uci', 'set', 'wireless.wlan1up=wifi-iface'],
|
['uci', 'set', 'wireless.wlan1up=wifi-iface'],
|
||||||
['uci', 'set', 'wireless.wlan1up.device=radio1'],
|
['uci', 'set', 'wireless.wlan1up.device=radio1'],
|
||||||
['uci', 'set', 'wireless.wlan1up.mode=sta'],
|
['uci', 'set', 'wireless.wlan1up.mode=sta'],
|
||||||
['uci', 'set', 'wireless.wlan1up.network=cli'],
|
['uci', 'set', 'wireless.wlan1up.network=cli'],
|
||||||
['uci', 'set', 'wireless.wlan1up.ssid=%s' % ssid],
|
['uci', 'set', 'wireless.wlan1up.ssid=%s' % ssid],
|
||||||
# sae-mixed = WPA2-PSK/WPA3-SAE transition; associates with
|
|
||||||
# either security mode. 'none' for open networks.
|
|
||||||
['uci', 'set', 'wireless.wlan1up.encryption=%s'
|
|
||||||
% ('sae-mixed' if psk else 'none')],
|
|
||||||
['uci', 'set', 'wireless.wlan1up.disabled=0'],
|
['uci', 'set', 'wireless.wlan1up.disabled=0'],
|
||||||
]
|
]
|
||||||
if psk:
|
if psk:
|
||||||
cmds.append(['uci', 'set', 'wireless.wlan1up.key=%s' % psk])
|
base_cmds.append(['uci', 'set',
|
||||||
for c in cmds:
|
'wireless.wlan1up.key=%s' % psk])
|
||||||
|
for c in base_cmds:
|
||||||
device_run(c)
|
device_run(c)
|
||||||
if _ensure_cli_network():
|
if _ensure_cli_network():
|
||||||
# netifd consumes committed config only; staging without commit
|
# netifd consumes committed config only; staging without commit
|
||||||
# would leave the STA with no L3 attachment.
|
# would leave the STA with no L3 attachment.
|
||||||
device_run(['uci', 'commit', 'network'])
|
device_run(['uci', 'commit', 'network'])
|
||||||
device_run(['uci', 'commit', 'wireless'])
|
|
||||||
_pause_hop()
|
_pause_hop()
|
||||||
device_run(['wifi', 'reload'], timeout=60)
|
|
||||||
assoc = None
|
assoc = None
|
||||||
for _ in range(ASSOC_ATTEMPTS):
|
used_mode = None
|
||||||
time.sleep(ASSOC_WAIT_SECONDS)
|
modes = PSK_MODE_CHAIN if psk else [('none', None)]
|
||||||
assoc = associated()
|
for enc, pmf in modes:
|
||||||
|
device_run(['uci', 'set', 'wireless.wlan1up.encryption=%s' % enc])
|
||||||
|
if pmf is not None:
|
||||||
|
device_run(['uci', 'set',
|
||||||
|
'wireless.wlan1up.ieee80211w=%s' % pmf])
|
||||||
|
device_run(['uci', 'commit', 'wireless'])
|
||||||
|
device_run(['wifi', 'reload'], timeout=60)
|
||||||
|
for _ in range(ASSOC_ATTEMPTS):
|
||||||
|
time.sleep(ASSOC_WAIT_SECONDS)
|
||||||
|
assoc = associated()
|
||||||
|
if assoc:
|
||||||
|
break
|
||||||
if assoc:
|
if assoc:
|
||||||
|
used_mode = enc
|
||||||
break
|
break
|
||||||
if not assoc:
|
if not assoc or not used_mode:
|
||||||
disable_uplink()
|
disable_uplink()
|
||||||
# UCI alone does not converge runtime: without a reload wlan1up
|
# UCI alone does not converge runtime: without a reload wlan1up
|
||||||
# keeps scanning/authenticating and pins phy1 until some unrelated
|
# keeps scanning/authenticating and pins phy1 until some unrelated
|
||||||
@@ -131,8 +142,11 @@ def set_role(role, ssid=None, psk=None):
|
|||||||
# Converge now like the idle branch, then reapply the hop policy.
|
# Converge now like the idle branch, then reapply the hop policy.
|
||||||
device_run(['wifi', 'reload'], timeout=60)
|
device_run(['wifi', 'reload'], timeout=60)
|
||||||
_resume_hop()
|
_resume_hop()
|
||||||
return {'ok': False, 'error': 'association failed; reverted'}
|
return {'ok': False,
|
||||||
return {'ok': True, 'role': 'uplink', 'assoc': assoc}
|
'error': 'association failed; reverted',
|
||||||
|
'tried_modes': [m for m, _ in modes]}
|
||||||
|
return {'ok': True, 'role': 'uplink', 'assoc': assoc,
|
||||||
|
'mode': used_mode}
|
||||||
# attack/idle: tear down the STA so radio1 is free again.
|
# attack/idle: tear down the STA so radio1 is free again.
|
||||||
disable_uplink()
|
disable_uplink()
|
||||||
_resume_hop()
|
_resume_hop()
|
||||||
|
|||||||
@@ -298,3 +298,30 @@ class RfPlanTest(unittest.TestCase):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
class PskModeChainTest(RfPlanTest):
|
||||||
|
def test_chain_tries_next_mode_on_failure(self):
|
||||||
|
# first mode (sae-mixed) never associates; second (sae) does
|
||||||
|
self.iw_fail_left = 5 # fail all link polls of attempt 1
|
||||||
|
result = mk8_rfplan.set_role('uplink', ssid='Net', psk='secret')
|
||||||
|
self.assertTrue(result['ok'], result)
|
||||||
|
self.assertEqual(result['mode'], 'sae')
|
||||||
|
reloads = [c for c in self.cmds if c.startswith('wifi reload')]
|
||||||
|
self.assertEqual(len(reloads), 2)
|
||||||
|
|
||||||
|
def test_chain_exhaustion_reports_modes(self):
|
||||||
|
self.iw_fail_left = 99
|
||||||
|
result = mk8_rfplan.set_role('uplink', ssid='Net', psk='secret')
|
||||||
|
self.assertFalse(result['ok'])
|
||||||
|
self.assertEqual(result['tried_modes'],
|
||||||
|
['sae-mixed', 'sae', 'psk2'])
|
||||||
|
|
||||||
|
def test_first_mode_success_records_mode(self):
|
||||||
|
result = mk8_rfplan.set_role('uplink', ssid='Net', psk='secret')
|
||||||
|
self.assertTrue(result['ok'])
|
||||||
|
self.assertEqual(result['mode'], 'sae-mixed')
|
||||||
|
self.assertIn('uci set wireless.wlan1up.ieee80211w=1', self.cmds)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user