10 KiB
Radio1 5GHz/6GHz Rogue AP — Design Record
- Date: 2026-08-17
- Status: Implemented (Tasks 1–4), on-device verification pending
- Owner: Hak5 WiFi Pineapple Pager expansion project
- Scope: Mark VIII WebUI (
http://172.16.52.1:8080/) running a rogue AP on the Pager's second radio (radio1= MT7921U Wi-Fi 6E) on 5GHz and 6GHz, with band-aware channel pickers, without breaking the stock Pager UI's control of its own interfaces.
1. Goal
Today Mark VIII's PineAP Open AP and Evil WPA pages configure only wlan0open
/ wlan0wpa on radio0 (2.4GHz) via the stock daemon's set_ap endpoint. The
Pager carries a second radio — an MT7921U Wi-Fi 6E on internal USB — that is
otherwise only used by the stock daemon's hopping monitor wlan1mon. This
feature lets Mark VIII run an Open AP or Evil WPA (WPA2-PSK / WPA3-SAE /
WPA3-OWE) on radio1 at 5GHz and 6GHz, configured directly via UCI, with a
UI that picks channels by band and enforces WPA3 on 6GHz.
2. Hardware findings
Verified against the committed implementation (server.py, views.js) and the
plan's on-device groundwork:
radio0— MT7628, 2.4GHz only. The stock PineAP daemon ownswlan0open,wlan0wpa,wlan0cli,wlan0mon,wlan0mgmt. This feature does not change that ownership.radio1— MT7921U Wi-Fi 6E on internal USB (phy1), capable of 2.4/5/6GHz. The stock daemon ownswlan1mon, a daemon-managed hopping monitor interface:pineapd.wlan1monhasbands='2,5,6'andhop='1'.- Because
radio1has a single channel shared by all its virtual interfaces, awlan1AP and the hoppingwlan1moncannot both be active with an operator-chosen channel — hence the hop pause/resume mechanism (§6). - UCI state (
wireless.radio1): stock defaults areband='5g',channel='auto',htmode='VHT80',countryset for the device region.
3. Band / channel model
server.py defines the authoritative mapping (helpers added near
_uci_wifi_iface):
| Band | BAND_* |
Channels | band_htmode |
band_radio |
|---|---|---|---|---|
| 2.4GHz | BAND_2G = '2.4' |
1–14 |
HT20 |
radio0 |
| 5GHz | BAND_5G = '5' |
36–177 |
VHT80 |
radio1 |
| 6GHz | BAND_6G = '6' |
181–233 (step 4) |
HE80 |
radio1 |
channel_band(ch)classifies 2.4GHz (1–14) first, then 5GHz (36–177), and only then 6GHz (1 ≤ ch ≤ 233and(ch − 1) % 4 == 0). Because 2.4 and 5GHz take precedence, the only reachable 6GHz channels are181, 185, …, 233(the low 6E channels1,5,…,177are swallowed by the 2.4/5GHz ranges). The UI's 6GHz group therefore offers exactly181..233 step 4.CHANNEL_BANDSmirrors this:range(1, 15),range(36, 178),range(181, 234, 4)— held consistent by tests.DFS_CHANNELS=52..64and100..144(step 4). DFS channels are surfaced to the operator in the UI with a(DFS)marker; this feature does not attempt radar-CAC handling on-device.channel_freq(band, ch): 2.4 →2412 + (ch−1)·5; 5 →5180 + (ch−36)·5; 6 →5955 + (ch−1)·5.band_htmodemaps 2.4/5/6 →HT20/VHT80/HE80(written towireless.radio1.htmode).band_radiomaps 2.4 →radio0, 5/6 →radio1.
4. API behavior
4.1 h_pineap_wifi_get_ap (POST /api/pineap/wifi/get_ap)
- If
wlan1openorwlan1wpaexists inwireless, state is read fromradio1(wlan1open/wlan1wpa); otherwise fromradio0(wlan0open/wlan0wpa) — the 2.4GHz response shape is unchanged. open.channelandwpa.channelare reported per interface, falling back to the owning radio's channel when the iface section has no channel option (regression-tested).wpa.enctypeis normalized topsk2/sae/owe.- A new
radio1object reports the rawwireless.radio1state:{band, channel, htmode, country}, withbandnormalized from2g/5g/6gto'2.4'/'5'/'6'(default'5'), andchannelleft as the raw string ('auto'or a channel number) — the UI converts;'auto'is not int-coerced.
4.2 h_pineap_wifi_set_ap (POST /api/pineap/wifi/set_ap)
channel is now accepted in both open and wpa payloads. Behavior matrix:
- 2.4GHz channels (1–14): exactly today's path — daemon
PUT /api/settings/wifi/set_apforwlan0open/wlan0wpafollowed by_apply_open_radio(which persistsradio0.channel/country). If awlan1*AP exists it is removed first (_remove_radio1_ap, §6) so a 2.4GHz save tears down a stale radio1 AP. - 5GHz (36–177) / 6GHz (181–233):
_apply_radio1_ap(below). - Mixed request: a request carrying both a 2.4GHz object and a 5/6GHz
object returns
400 'cannot configure 2.4GHz and radio1 APs in one request'(radio1 is one physical radio — one band/channel per request). - Disable: a radio1 request with no active 5/6GHz object (or a 2.4GHz save)
runs
_remove_radio1_ap()+wifi reloadand returns200.
_apply_radio1_ap(openap, wpa) (one of the two is active):
- Validates the band — a radio1 AP requires a 5GHz or 6GHz channel
(
ValueError→ HTTP 400). - 6GHz requires WPA3: when the active AP is a WPA AP on 6GHz,
enctypemust besaeorowe;psk2is rejected with HTTP 400 ('6GHz requires WPA3 (sae or owe)'). An open 6GHz AP is accepted by the backend, but the UI warns that real clients generally won't associate to an open 6GHz network. - Deletes any existing
wlan1open/wlan1wpa(idempotent), then writes UCI:wireless.radio1.band=5g/6g,wireless.radio1.channel,wireless.radio1.htmode(band_htmode),wireless.radio1.country(when supplied);- a
wifi-ifacesectionwlan1open(encryptionnone, optional BSSID) orwlan1wpa(encryption+key) ondevice=radio1,mode=ap, with the interface-levelchannel/hidden/ssid.
uci commit wireless,_pause_hop()(§6), thenwifi reload.
5. Coexistence rules — "don't break stock"
- Mark VIII owns:
wireless.wlan1open,wireless.wlan1wpa, andwireless.radio1.{channel,band,htmode,country}. These are new sections / values it creates and tears down. - Stock owns (never modified by Mark VIII):
wireless.wlan0open,wlan0wpa,wlan0mgmt,wlan0cli,wlan0mon,wireless.wlan1mon, and allpineapd.*UCI values (bands configuration included). - The only stock-owned value this feature writes is
pineapd.wlan1mon.hop— and it is always restored to its prior value (_resume_hopsets it back to1only if it was0;_pause_hopsets it to0only if it was not already0). - The 2.4GHz daemon path (
PUT /api/settings/wifi/set_apforwlan0open/wlan0wpa) is byte-for-byte unchanged. - Known risk (accepted): the stock daemon's
set_ap/pager-UI writes may rewritewirelesswholesale and drop thewlan1*sections. Mitigation is UCI-commit persistence, hop restore on disable, and on-device verification (§9). If clobbering is observed, the deferred fix is a reconcile-on-load step inget_apthat re-applies a saved radio1 AP fromPINEAP_STATE_FILE.
6. Hop pause / resume
wlan1mon is the stock daemon's channel-hopping monitor. With a radio1 AP
active, hopping would fight the AP's fixed channel, so it is paused while the
AP is enabled:
_read_hop()reads the value viauci get pineapd.wlan1mon.hop(a leaf read — not_uci_wifi_iface, which forces thewireless.prefix)._pause_hop(): ifhop != '0', setpineapd.wlan1mon.hop=0,uci commit pineapd, reload/etc/init.d/pineapd._resume_hop(): ifhop == '0', set it back to1, commit, reload._apply_radio1_apcalls_pause_hop()beforewifi reload;_remove_radio1_apcalls_resume_hop()after resettingradio1.channel=auto/radio1.band=5g. Every code path that pauses hopping also restores it.
7. Frontend (www/js/views.js)
BAND_GROUPSdrives the channel pickers shared by the Open AP and Evil WPA views: a2.4 GHzoptgroup (1–11), a5 GHzoptgroup (36–177, DFS channels52..64/100..144labelled(DFS)), and a6 GHz (WPA3/OWE only)optgroup (181..233step 4).chanFreq/chanLabelrenderChannel N (… MHz)(+(DFS)),chanSelectbuilds the optgroups and restores a stored value when in range,bandOfChannelmirrorschannel_band.- Open AP: channel select + a hint that appears on 6GHz ("…most devices will not associate to an open 6 GHz network.").
- Evil WPA: a channel select added to the config card; selecting a 6GHz
channel disables the
psk2option and switches tosae, with a "6 GHz requires WPA3 (SAE or OWE)." hint. Save payloads for both views includechannel(Open AP alsocountry).
8. Automated verification
tests/test_pineap_bands.pycovers the channel/band helpers (ChannelBandTest,ChannelBandsConsistencyTest,ChannelFreqTest,BandAuxTestincl. DFS marker),get_ap(GetApRadio1Test,GetApRadio1AbsentTest,GetApRadioChannelFallbackTest) andset_ap(SetApRadio1Test: 5GHz open writesradio1sections + hop pause; 6GHz WPA3-SAE accepted; 6GHzpsk2rejected; disable removes the radio1 AP and restores hop; 2.4GHz still uses the daemon path; 2.4GHz save removes a stale radio1 AP; mixed 2.4GHz + radio1 rejected).- All 14 test modules pass at HEAD (
ff3bd16), run per-module in separate processes per the repo convention (test_auth,test_core,test_loot,test_misc,test_pineap_bands,test_pineap_clients,test_pineap_enterprise,test_pineap_modes,test_pineap_pool,test_pineap_proxy,test_pineap_settings,test_recon,test_status,test_ws).
9. On-device verification
On-device verification is pending (Task 5 of the implementation plan,
deferred until it can be run against the user's Pager at 172.16.52.1 without
colliding with other agents' deployed builds). Planned checks: 2.4GHz behavior
unchanged (wlan0open/radio0/wlan1mon untouched), 5GHz Evil WPA
(WPA3-SAE) bringing up wlan1wpa with hop='0', stock Pager UI toggles
continuing to work, 5GHz handshake capture, reboot persistence, disable path
restoring hopping, and 5GHz Open AP. Results will be recorded here once
complete.