Files
Mark-VIII/docs/specs/2026-08-17-radio1-5ghz6ghz-ap.md

12 KiB
Raw Permalink Blame History

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 owns wlan0open, 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 owns wlan1mon, a daemon-managed hopping monitor interface: pineapd.wlan1mon has bands='2,5,6' and hop='1'.
  • Because radio1 has a single channel shared by all its virtual interfaces, a wlan1 AP and the hopping wlan1mon cannot both be active with an operator-chosen channel — hence the hop pause/resume mechanism (§6).
  • UCI state (wireless.radio1): stock defaults are band='5g', channel='auto', htmode='VHT80', country set 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' 114 HT20 radio0
5GHz BAND_5G = '5' 36177 VHT80 radio1
6GHz BAND_6G = '6' 181233 (step 4) HE80 radio1
  • channel_band(ch) classifies 2.4GHz (114) first, then 5GHz (36177), and only then 6GHz (1 ≤ ch ≤ 233 and (ch 1) % 4 == 0). Because 2.4 and 5GHz take precedence, the only reachable 6GHz channels are 181, 185, …, 233 (the low 6E channels 1,5,…,177 are swallowed by the 2.4/5GHz ranges). The UI's 6GHz group therefore offers exactly 181..233 step 4.
  • CHANNEL_BANDS mirrors this: range(1, 15), range(36, 178), range(181, 234, 4) — held consistent by tests.
  • DFS_CHANNELS = 52..64 and 100..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 + (ch1)·5; 5 → 5180 + (ch36)·5; 6 → 5955 + (ch1)·5.
  • band_htmode maps 2.4/5/6 → HT20 / VHT80 / HE80 (written to wireless.radio1.htmode). band_radio maps 2.4 → radio0, 5/6 → radio1.

4. API behavior

4.1 h_pineap_wifi_get_ap (POST /api/pineap/wifi/get_ap)

  • If wlan1open or wlan1wpa exists in wireless, state is read from radio1 (wlan1open/wlan1wpa); otherwise from radio0 (wlan0open/wlan0wpa) — the 2.4GHz response shape is unchanged.
  • open.channel and wpa.channel are reported per interface, falling back to the owning radio's channel when the iface section has no channel option (regression-tested). wpa.enctype is normalized to psk2 / sae / owe.
  • A new radio1 object reports the raw wireless.radio1 state: {band, channel, htmode, country}, with band normalized from 2g/5g/6g to '2.4'/'5'/'6' (default '5'), and channel left 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 (114): exactly today's path — daemon PUT /api/settings/wifi/set_ap for wlan0open/wlan0wpa followed by _apply_open_radio (which persists radio0.channel/country). If a wlan1* AP exists it is removed first (_remove_radio1_ap, §6) so a 2.4GHz save tears down a stale radio1 AP.
  • 5GHz (36177) / 6GHz (181233): _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 reload and returns 200.

_apply_radio1_ap(openap, wpa) (one of the two is active):

  1. Validates the band — a radio1 AP requires a 5GHz or 6GHz channel (ValueError → HTTP 400).
  2. 6GHz requires WPA3: when the active AP is a WPA AP on 6GHz, enctype must be sae or owe; psk2 is 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.
  3. 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-iface section wlan1open (encryption none, optional BSSID) or wlan1wpa (encryption + key) on device=radio1, mode=ap, with the interface-level channel/hidden/ssid.
  4. uci commit wireless, _pause_hop() (§6), then wifi reload.

5. Coexistence rules — "don't break stock"

  • Mark VIII owns: wireless.wlan1open, wireless.wlan1wpa, and wireless.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 all pineapd.* 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_hop sets it back to 1 only if it was 0; _pause_hop sets it to 0 only if it was not already 0).
  • The 2.4GHz daemon path (PUT /api/settings/wifi/set_ap for wlan0open / wlan0wpa) is byte-for-byte unchanged.
  • Known risk (accepted): the stock daemon's set_ap/pager-UI writes may rewrite wireless wholesale and drop the wlan1* 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 in get_ap that re-applies a saved radio1 AP from PINEAP_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 via uci get pineapd.wlan1mon.hop (a leaf read — not _uci_wifi_iface, which forces the wireless. prefix).
  • _pause_hop(): if hop != '0', set pineapd.wlan1mon.hop=0, uci commit pineapd, reload /etc/init.d/pineapd.
  • _resume_hop(): if hop == '0', set it back to 1, commit, reload.
  • _apply_radio1_ap calls _pause_hop() before wifi reload; _remove_radio1_ap calls _resume_hop() after resetting radio1.channel=auto / radio1.band=5g. Every code path that pauses hopping also restores it.

7. Frontend (www/js/views.js)

  • BAND_GROUPS drives the channel pickers shared by the Open AP and Evil WPA views: a 2.4 GHz optgroup (111), a 5 GHz optgroup (36165, DFS channels 52..64 / 100..144 labelled (DFS)), and a 6 GHz (WPA3/OWE only) optgroup (181..233 step 4).
  • chanFreq/chanLabel render Channel N (… MHz) (+ (DFS)), chanSelect builds the optgroups and restores a stored value when in range, bandOfChannel mirrors channel_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 psk2 option and switches to sae, with a "6 GHz requires WPA3 (SAE or OWE)." hint. Save payloads for both views include channel (Open AP also country).

8. Automated verification

  • tests/test_pineap_bands.py covers the channel/band helpers (ChannelBandTest, ChannelBandsConsistencyTest, ChannelFreqTest, BandAuxTest incl. DFS marker), get_ap (GetApRadio1Test, GetApRadio1AbsentTest, GetApRadioChannelFallbackTest) and set_ap (SetApRadio1Test: 5GHz open writes radio1 sections + hop pause; 6GHz WPA3-SAE accepted; 6GHz psk2 rejected; 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/radio0 intact and pineapd.wlan1mon untouched (bands=2,5,6, hop=1).
  • 5GHz Evil WPA (WPA3-SAE, channel 36, VHT80): radio1.band=5g, channel=36, htmode=VHT80; wlan1wpa (netdev named wlan1wpa via option ifname) comes up beaconing Test5G / WPA3 SAE (CCMP); pineapd.wlan1mon.hop=0. get_ap reports wpa.enabled=true (after the disabled=0 fix).
  • 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; the wlan1wpa UCI section survives and get_ap self-heals it with a wifi reload (/sys/class/net/<iface> missing check). Under rapid reload churn the mt7921u driver can transiently return EBUSY; a later reload succeeds. The pager UI itself is unaffected.
  • Handshake capture: Examine on channel 36 returns success; handshake logging (loghandshake/logpartialhandshake) confirmed on. A live WPA handshake file requires a physical client (not exercised).
  • Reboot persistence: wlan1wpa UCI, disabled=0, hop=0, the procd Mark VIII service, and the AP itself all survive reboot.
  • Disable path: removing the 5GHz AP deletes wlan1wpa, resets radio1.channel=auto/band=5g, restores hop=1; wlan1mon hopping resumes (observed 6GHz ch13 → ch221 in 30s).
  • 5GHz Open AP: wlan1open (channel 44, open) brings up Test5GOpen with hop=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.