feat(reliability): phy1 RF role manager with uplink-on-radio1

This commit is contained in:
2026-08-22 14:20:05 -06:00
parent 78aab64af0
commit 15cd3c5eb8
3 changed files with 282 additions and 0 deletions
@@ -0,0 +1,87 @@
"""Mark VIII RF role manager: radio1/phy1 is shared between an uplink STA
(``wlan1up``) and attack work, so the roles are made mutually exclusive.
Uplink pauses channel hopping; attack/idle resumes it."""
ROLE_KEY = 'mk8.rfplan.role'
IFACE = 'wlan1up'
def current_role():
from server import _uci_values
cfg = _uci_values('wireless.%s' % IFACE) or {}
if cfg.get('disabled') != '1' and cfg.get('mode') == 'sta':
return 'uplink'
return 'idle'
def associated():
"""BSSID of the uplink AP when wlan1up is associated, else None."""
from server import device_run
rc, out, err = device_run(['iw', 'dev', IFACE, 'link'], timeout=10)
if rc != 0 or 'Connected' not in (out or ''):
return None
for line in (out or '').splitlines():
line = line.strip()
if line.startswith('Connected to '):
return line.split()[2]
return None
def hop_paused():
from server import _read_hop
return _read_hop() == '0'
def set_role(role, ssid=None, psk=None):
from server import device_run, _pause_hop, _resume_hop
if role not in ('uplink', 'attack', 'idle'):
return {'ok': False, 'error': 'role must be uplink, attack or idle'}
if role == 'uplink':
if not ssid:
return {'ok': False, 'error': 'ssid required'}
cmds = [
['uci', 'set', 'wireless.wlan1up=wifi-iface'],
['uci', 'set', 'wireless.wlan1up.device=radio1'],
['uci', 'set', 'wireless.wlan1up.mode=sta'],
['uci', 'set', 'wireless.wlan1up.network=cli'],
['uci', 'set', 'wireless.wlan1up.ssid=%s' % ssid],
['uci', 'set', 'wireless.wlan1up.encryption=%s'
% ('psk2' if psk else 'none')],
['uci', 'set', 'wireless.wlan1up.disabled=0'],
]
if psk:
cmds.append(['uci', 'set', 'wireless.wlan1up.key=%s' % psk])
for c in cmds:
device_run(c)
# The STA rides network 'cli'; stock firmware ships it disabled but
# present. If it is missing entirely, create a minimal DHCP interface
# so netifd can bring wlan1up up.
rc, _, _ = device_run(['uci', 'show', 'network.cli'])
if rc != 0:
device_run(['uci', 'set', 'network.cli=interface'])
device_run(['uci', 'set', 'network.cli.proto=dhcp'])
device_run(['uci', 'commit', 'wireless'])
_pause_hop()
device_run(['wifi', 'reload'], timeout=60)
assoc = associated()
if not assoc:
disable_uplink()
_resume_hop()
return {'ok': False, 'error': 'association failed; reverted'}
return {'ok': True, 'role': 'uplink', 'assoc': assoc}
# attack/idle: tear down the STA so radio1 is free again.
disable_uplink()
_resume_hop()
return {'ok': True, 'role': role}
def disable_uplink():
from server import device_run
device_run(['uci', 'set', 'wireless.wlan1up.disabled=1'])
device_run(['uci', 'commit', 'wireless'])
def ensure_attack():
"""Exclusivity hook for attack enable paths: switch uplink off first."""
if current_role() == 'uplink':
return set_role('attack')
return None
@@ -4812,6 +4812,44 @@ def start_health_monitor():
threading.Thread(target=_health_loop, daemon=True).start()
# --------------------------------------------------------------------------
# RF role manager: radio1 shared between uplink STA and attack work.
# --------------------------------------------------------------------------
def h_rfplan_get(ctx):
import mk8_rfplan
return 200, {'role': mk8_rfplan.current_role(),
'assoc': mk8_rfplan.associated(),
'hop_paused': mk8_rfplan.hop_paused()}
def h_rfplan_post(ctx):
import mk8_gate
import mk8_events
import mk8_rfplan
body = ctx.body or {}
role = (body.get('role') or '').strip().lower()
if role not in ('uplink', 'attack', 'idle'):
return 400, {'error': 'role must be uplink, attack or idle'}
mk8_gate.enter('rfplan_' + role)
try:
result = mk8_rfplan.set_role(
role,
ssid=(body.get('ssid') or '').strip() or None,
psk=(body.get('psk') or '').strip() or None)
except Exception as exc:
result = {'ok': False, 'error': str(exc)}
ok = bool(result.get('ok'))
try:
mk8_events.log_event('rfplan', sev='info' if ok else 'warn',
msg='rfplan role %s %s'
% (role, 'applied' if ok else 'failed'),
meta=result)
except Exception:
pass
return (200, result) if ok else (502, result)
# --------------------------------------------------------------------------
# Startup environment check: run once at service startup (and via
# ``server.py --env-check`` on the payload screen) to make the device match
@@ -7194,6 +7232,8 @@ def h_mode_release(ctx):
ROUTER.add('GET', r'/api/mode', h_mode_get)
ROUTER.add('POST', r'/api/mode/release', h_mode_release)
ROUTER.add('GET', r'/api/rfplan', h_rfplan_get)
ROUTER.add('POST', r'/api/rfplan/role', h_rfplan_post)
def serve():