docs: 5GHz/6GHz rogue AP feature and coexistence notes
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user