12 KiB
Radio1 5GHz/6GHz Rogue AP — Design Record
- Date: 2026-08-17
- Status: Implemented and verified on-device (see §9)
- 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–165, 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 (
af5ff80), 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
Run against the user's Pager at 172.16.52.1 (Pineapple Pager 24.10.1). All
checks passed:
- 2.4GHz unchanged: Open AP save (channel 1) leaves
wlan0open/radio0intact andpineapd.wlan1monuntouched (bands=2,5,6,hop=1). - 5GHz Evil WPA (WPA3-SAE, channel 36, VHT80):
radio1.band=5g,channel=36,htmode=VHT80;wlan1wpa(netdev namedwlan1wpaviaoption ifname) comes up beaconingTest5G/ WPA3 SAE (CCMP);pineapd.wlan1mon.hop=0.get_apreportswpa.enabled=true(after thedisabled=0fix). - Stock Pager UI coexistence: the stock daemon's own
set_ap(what the pager UI uses to change the 2.4GHz Evil WPA) tears down the radio1 AP netdev; thewlan1wpaUCI section survives andget_apself-heals it with awifi reload(/sys/class/net/<iface>missing check). Under rapid reload churn themt7921udriver can transiently return EBUSY; a later reload succeeds. The pager UI itself is unaffected. - Handshake capture:
Examineon channel 36 returns success; handshake logging (loghandshake/logpartialhandshake) confirmed on. A live WPA handshake file requires a physical client (not exercised). - Reboot persistence:
wlan1wpaUCI,disabled=0,hop=0, the procd Mark VIII service, and the AP itself all survive reboot. - Disable path: removing the 5GHz AP deletes
wlan1wpa, resetsradio1.channel=auto/band=5g, restoreshop=1;wlan1monhopping resumes (observed 6GHz ch13 → ch221 in 30s). - 5GHz Open AP:
wlan1open(channel 44, open) brings upTest5GOpenwithhop=0. - 6GHz AP:
radio1.band=6g,htmode=HE80, WPA3 SAE on channel 181 (6.855 GHz) comes up. - Cleanup: disable restores the 2.4GHz baseline (
pager-open, channel 1,hop=1).
Known follow-ups: the Clients tab lists only wlan0* interfaces, so 5GHz AP
clients are not yet shown; live-client handshake capture is untested without a
physical client.