diff --git a/README.md b/README.md index eaf5997..ed33c1a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ Recon (scans from `recon.db`), Handshakes/Loot, Payloads (embedded stock Pager Portal), Logs, Settings (hostname/NTP/password/prefs), and a bottom-docked xterm terminal. +- Rogue AP on the second radio (5GHz / 6GHz Wi-Fi 6E): Open AP and Evil WPA + (WPA2-PSK/WPA3-SAE/WPA3-OWE) on `radio1`, band-aware channel pickers, + 6GHz requires WPA3. While a radio1 AP is enabled the stock monitor-hopping + (`wlan1mon`) is paused and resumed on disable; 2.4GHz PineAP is untouched. + ## Requirements - WiFi Pineapple Pager, firmware `Pineapple Pager 24.10.1` diff --git a/docs/specs/2026-08-17-radio1-5ghz6ghz-ap.md b/docs/specs/2026-08-17-radio1-5ghz6ghz-ap.md new file mode 100644 index 0000000..dbe8fd4 --- /dev/null +++ b/docs/specs/2026-08-17-radio1-5ghz6ghz-ap.md @@ -0,0 +1,191 @@ +# 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 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'` | `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 ≤ 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 + (ch−1)·5`; 5 → `5180 + (ch−36)·5`; + 6 → `5955 + (ch−1)·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 (1–14):** 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 (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 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 (1–11), a `5 GHz` optgroup (36–177, 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 (`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.