From 501fa455ceb0d2ae6cc05fdff020ee478b8c9c79 Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Sat, 22 Aug 2026 20:37:11 -0600 Subject: [PATCH] fix(reliability): resolve STA netdev by phy membership (netifd ignores ifname); sae-mixed for PSK uplinks --- .../remote_access/pager-webui/mk8_rfplan.py | 39 +++++++++++++++++-- tests/test_mk8_rfplan.py | 13 ++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/payload/user/remote_access/pager-webui/mk8_rfplan.py b/payload/user/remote_access/pager-webui/mk8_rfplan.py index 1028436..0994e64 100644 --- a/payload/user/remote_access/pager-webui/mk8_rfplan.py +++ b/payload/user/remote_access/pager-webui/mk8_rfplan.py @@ -20,10 +20,39 @@ def current_role(): return 'idle' -def associated(): - """BSSID of the uplink AP when wlan1up is associated, else None.""" +def _sta_netdev(): + """Actual netdev carrying the radio1 STA. OpenWrt ignores a requested + ifname for mac80211 STA ifaces (comes up as phy1-sta0), so resolve by + phy membership + managed type instead of by name.""" from server import device_run - rc, out, err = device_run(['iw', 'dev', IFACE, 'link'], timeout=10) + rc, out, err = device_run(['iw', 'dev'], timeout=10) + if rc != 0: + return None + current = None + managed = [] + for line in (out or '').splitlines(): + line = line.strip() + if line.startswith('Interface '): + current = line.split()[1] + elif line.startswith('type managed') and current: + if not current.startswith('wlan0'): + managed.append(current) + current = None + for name in managed: + rc2, o2, _ = device_run( + ['readlink', '/sys/class/net/%s/phy80211' % name], timeout=10) + if rc2 == 0 and 'phy1' in (o2 or ''): + return name + return None + + +def associated(): + """BSSID of the uplink AP when the radio1 STA is associated, else None.""" + from server import device_run + dev = _sta_netdev() + if not dev: + return None + rc, out, err = device_run(['iw', 'dev', dev, 'link'], timeout=10) if rc != 0 or 'Connected' not in (out or ''): return None for line in (out or '').splitlines(): @@ -71,8 +100,10 @@ def set_role(role, ssid=None, psk=None): ['uci', 'set', 'wireless.wlan1up.mode=sta'], ['uci', 'set', 'wireless.wlan1up.network=cli'], ['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' - % ('psk2' if psk else 'none')], + % ('sae-mixed' if psk else 'none')], ['uci', 'set', 'wireless.wlan1up.disabled=0'], ] if psk: diff --git a/tests/test_mk8_rfplan.py b/tests/test_mk8_rfplan.py index 6738790..e155da9 100644 --- a/tests/test_mk8_rfplan.py +++ b/tests/test_mk8_rfplan.py @@ -38,7 +38,16 @@ class RfPlanTest(unittest.TestCase): args = list(args) self.seq.append('cmd:' + ' '.join(str(a) for a in args)) self.runs.append((args, timeout)) + if args == ['iw', 'dev']: + # interface enumeration: radio1 STA comes up as phy1-sta0 + return 0, ('Interface wlan1mon\n' + '\ttype monitor\n' + 'Interface phy1-sta0\n' + '\ttype managed\n'), '' + if args[0] == 'readlink' and 'phy80211' in args[1]: + return 0, '../../devices/platform/usb/phy1\n', '' if args[0] == 'iw': + # iw dev link if self.iw_fail_left > 0: self.iw_fail_left -= 1 return 0, 'Not connected.\n', '' @@ -108,7 +117,7 @@ class RfPlanTest(unittest.TestCase): 'uci set wireless.wlan1up.mode=sta', 'uci set wireless.wlan1up.network=cli', 'uci set wireless.wlan1up.ssid=Net', - 'uci set wireless.wlan1up.encryption=psk2', + 'uci set wireless.wlan1up.encryption=sae-mixed', 'uci set wireless.wlan1up.key=key', 'uci set wireless.wlan1up.disabled=0', 'uci set network.cli=interface', @@ -146,7 +155,7 @@ class RfPlanTest(unittest.TestCase): self.assertEqual(result['assoc'], 'aa:bb:cc:dd:ee:ff') self.assertEqual(len(self.sleeps), 3) iw_calls = [c for c in self.cmds - if c.startswith('iw dev wlan1up link')] + if c.startswith('iw dev phy1-sta0 link')] self.assertEqual(len(iw_calls), 3) def test_assoc_poll_exhaustion_reverts_uplink(self):