diff --git a/CMakeLists.txt b/CMakeLists.txt index ceb4882b..77b83465 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -374,6 +374,7 @@ endif() if(DEVOURER_MT7612U) target_sources(devourer PRIVATE src/mt7612u/async.cpp + src/mt7612u/beacon.cpp src/mt7612u/caps.cpp src/mt7612u/eeprom.cpp src/mt7612u/fw.cpp diff --git a/docs/mt7612u-ap-mode.md b/docs/mt7612u-ap-mode.md new file mode 100644 index 00000000..06c173c2 --- /dev/null +++ b/docs/mt7612u-ap-mode.md @@ -0,0 +1,304 @@ +# MT7612U as a fully-userspace access point + +**Status: it works.** devourer's own AP harnesses run against this part +unmodified — beacon, probe, auth, assoc, and the ARP/ICMP data plane — with a +real Linux station associated. `StartBeacon`, `UpdateBeaconPayload` and +`StopBeacon` are implemented on `Mt7612uRadio`, so nothing in +`tests/ap_responder.cpp` or `tests/ap_wpa2.cpp` branches on the backend. (Both +carry one MediaTek-specific *comment* now, explaining why they silence the +beacon before `_exit`; no code depends on it.) + +**WPA2-PSK works too.** `tests/ap_wpa2.cpp`, also unmodified, completes the +4-way handshake against a real `wpa_supplicant` station and carries encrypted +traffic. It needs the same five `IRadio` methods as the open-network harness - +`InitWrite`, `StartBeacon`, `StartRxLoop`, `send_packet`, `StopBeacon` - and no +others. CCMP is done in software in the harness, so no key API is involved; +that is a separate point from the method count. + +What is NOT done: **hardware** CCMP. `MT_WCID_KEY` and `MT_SKEY` are untouched, +so the claim below that hardware crypto is a capability *gain* on this part +remains unmeasured — what is measured is that the software path devourer +already had works here. Key install is the one item of the original gap list +that is still open. + +File:line references below are to the merged subtree (`src/mt7612u/`, all +`.cpp` since the C++ migration) and to `reference/mt76 @ be5ce79`. Some of the +citations in "The gap" section still name the pre-migration `.c` filenames and +pre-merge line numbers; they are kept because the reasoning is still correct, +but do not expect them to resolve. + +## Verified on hardware (2026-09-08, MT7612U at USB 2-1) + +Measured against a second MT7612U bound to the kernel `mt76x2u` driver as a +station, plus an RTL8812AU running `rxdemo` as an independent on-air witness. + +| Claim | Evidence | +|---|---| +| Beacon reaches the air | Kernel station's `iw scan` lists `SSID: MT7612U-AP`, `beacon interval: 100 TUs`, `capability: ESS (0x0001)`, correct `DS Parameter set` and basic-rate flags — on ch149, ch36 **and** ch6 (both bands) | +| Independent radio decodes it | 8812AU witness: 177 frames on a channel measured empty beforehand, each `len:71` (our 67-byte MPDU + FCS) at `rate:4` (OFDM 6M), ~9.8/s | +| HW TSF timestamp (`FLAGS_TS`) | `wlan.fixed.timestamp` advances **102400 µs** per beacon — exactly 100 TU | +| HW sequence (`ACK_CTL_NSEQ`) | `wlan.seq` increments **+1 per beacon** (2140, 2141, 2142 …) | +| Beacon interval math | On-air spacing 102.4 ms, confirming `INTVAL = interval_tu << 4` (1/16 TU) | +| Corrected MBSS masks | `MT_MAC_BSSID_DW1` reads `0x003fa127` — upper bits exactly mt76's `MBSS_MODE=3 / MBEACON_N=7 / LOCAL_BIT` | +| Hardware auto-ACK (Gate B) | A real station's **3 auth frames, 0 retried**. An un-ACKed frame is retransmitted with FC Retry set, so retry=0 is the ACK | +| APC BSSID slot programmed | `MT_MAC_APC_BSSID_L(0)=0x50efa540` (device MAC `40:a5:ef:50:…`) | + +Not yet done: probe **responses**, auth/assoc **responses** and the data plane — +those are the existing backend-agnostic C++ harnesses' job (Stages C–E), not +driver work. + +## Verified through IRadio (2026-09-10) — devourer as the AP + +The section above is the bring-up gates driving the C library directly. This +one is devourer itself: `tests/ap_responder.cpp`, unmodified, built against +`libdevourer.a` and pointed at an MT7612U. A second MT7612U on the kernel +`mt76x2u` driver is the station. ch36, `iw reg set SE`. + +| Claim | Evidence | +|---|---| +| `StartBeacon` arms the MAC | `MT7612U beaconing every 100 TU`, then `ap_responder up on ch36 SSID devourerAP (beacon OK)` | +| The beacon is on air and correct | station `iw scan`: `SSID: devourerAP`, `BSS 02:42:75:05:d6:00`, `beacon interval: 100 TUs`, `capability: ESS (0x0001)`, `DS Parameter set: channel 36`, −32 dBm | +| A locally-administered BSSID works | that BSSID is `02:…`, so it lands in APC slot 1 by mt76's rule. The first draft of `mt7612u_beacon_start()` refused it outright | +| A real station associates | `wlx…: connected to 02:42:75:05:d6:00`, `freq: 5180.0` | +| The MAC auto-ACKs | AP side, three runs: `AUTH req … alg=0 seq=1 retry=0` and `ASSOC req … retry=0`. An un-ACKed frame is retransmitted with FC Retry set, so retry=0 IS the ACK | +| The data plane works | `6 packets transmitted, 6 received, 0% packet loss, rtt avg 0.808 ms`; AP side `data(arp/icmp)=8 responses_sent=16` | +| `StopBeacon` silences it | `tests/mt7612u_beacon_stop_check.cpp`: armed → SSID seen; stopped → gone; re-armed → seen again | +| WPA2-PSK 4-way completes | AP side: `msg2 OK (SNonce, MIC verified) — PTK derived`, `sent msg3 (GTK, MIC)`, `msg4 OK — 4-WAY HANDSHAKE COMPLETE (station keyed)` against `wpa_supplicant` with `proto=RSN pairwise=CCMP group=CCMP` | +| Encrypted traffic flows | `6 packets transmitted, 6 received, 0% packet loss, rtt avg 1.156 ms` after the handshake — which requires both ends to agree on CCMP | + +### What this does not show + +- **The encryption was not independently captured.** The 4-way completing with + a verified MIC, and traffic flowing to a CCMP-only station, is strong + evidence that frames are protected — but no third radio sniffed the air to + confirm the Protected bit directly, and `wpa_cli` could not be queried for + the negotiated cipher (the supplicant was started without a control socket). +- **Hardware CCMP is untested.** The 4-way above is devourer's software CCMP, + the same code the Realtek backends use. `MT_WCID_KEY` / `MT_SKEY` are not + wired up, so the "crypto becomes hardware on MediaTek" claim is still a + claim. +- **The station is the same silicon** (MT7612U on `mt76x2u`), so this is not an + independent-generation witness. The RTL8812AU witness in the section above is. +- **One AP, one station, ~20 cm apart.** Every RSSI here is near-field. +- **Longest run 70 s.** No soak, no second station, no rekey, no roaming, and + no channel change while beaconing. +- **`iw scan` alone is not a witness for a beacon *stopping*.** Its BSS cache + holds an entry ~30 s after the beacon dies, and it reported a stopped beacon + as present until `iw scan flush` was used. A re-arm also takes long enough + (a 1600-byte page copy over EP0) that a scan at +8 s still misses it. +- **The beacon-stop evidence is from a purpose-built harness, not from process + exit.** Both AP harnesses used to end in `_exit(0)`, skipping the destructor, + so `StopBeacon` never ran and "the SSID was gone after exit" measured nothing + - it looked true once, by luck, and was false. They call `StopBeacon` + explicitly now, and `tests/mt7612u_beacon_stop_check.cpp` is what actually + exercises the transition. + +## The claim, and why it holds + +The gap between "MT7612U injector" (what the subtree is) and "MT7612U userspace +AP" is small — a few hundred lines of C in the backend, and no new AP logic at +all. Two reasons: + +1. **devourer already has the AP brain, and it is backend-agnostic.** The + probe/auth/assoc responder, the DHCP/ARP/ICMP data plane, and the WPA2 4-way + handshake with software CCMP all live in `tests/` (`ap_responder.cpp`, + `ap_wpa2.cpp`, `probe_responder.cpp`, `beacon_*.cpp`), driven entirely + through the `IRadio` interface — `StartBeacon` + the RX callback + + `send_packet`. `docs/ap-mode.md` documents a complete open and WPA2-PSK AP + validated against real Linux stations on this stack. None of it is + Realtek-specific; it works against any backend that implements the beacon + and ACK primitives. + +2. **MediaTek's MAC offloads in hardware exactly what an AP needs most.** The + beacon is auto-transmitted from a reserved page at each TBTT, TSF-stamped by + the MAC; ACK is SIFS-timed by the MAC against the programmed address; + 802.11 sequence numbering is a MAC function; and CCMP has real per-station + key hardware (`MT_WCID_KEY`, `MT_SKEY`). On Realtek, devourer does CCMP in + **software** because the security TX-desc field is absent on most + generations (`docs/ap-mode.md`: "only Jaguar1 has + `SET_TX_DESC_SEC_TYPE_8812`"). So on MT the encrypted data plane, and GTK + rekey which is explicitly out of scope on Realtek, become **hardware** — + this part is a capability *gain*, not a gap. + +## What the MT7612U backend already has + +Verified in the merged subtree: + +| AP need | present today | where | +|---|---|---| +| Port MAC + BSSID programmed | yes — `MT_MAC_ADDR_DW0/1`, `MT_MAC_BSSID_DW0/1`, MBSS_MODE=3, MBEACON_N | `init.c:206‑216` (`mac_setaddr`) | +| Station table (WCID) | yes — `mt_wcid_setup(idx, mac)` writes `MT_WCID_ATTR` + address; all zeroed at init | `tx.c:95`, `init.c:236` | +| Crypto key slots | the shared-key store is present and zeroed at init (`MT_SKEY`, `MT_SKEY_MODE`, `src/mt7612u/init.cpp` `wcid_and_key_clear()`). The per-station key store is NOT defined in this tree - `MT_WCID_KEY` does not exist here, which is part of why hardware CCMP is unreached | +| ACKed unicast TX | yes — `no_ack=0` sets `MT_TXWI_ACK_CTL_REQ`; BA-window field present | `tx.c:164‑167` | +| Beacon-interval timer regs | defined — `MT_BEACON_TIME_CFG` INTVAL/TIMER_EN/TBTT_EN/BEACON_TX, `MBEACON_N` | `regs.h:176‑180,169` | +| RX filter control | yes — managed default `0x00015f97`, monitor clears to error-only | `init.c:278,494‑509` | +| Register block copy | yes — `mt_wr_copy()` for reserved-page writes | used in `init.c` | + +So the addressing, the station table, the crypto slots, the ACK path and the +beacon *timer* are already in place. The receiver runs (per the #414 tick), and +`mt_tx_build()` already produces `[TXWI][802.11]` which is exactly the reserved- +page beacon shape. + +## The gap — the driver primitives to add + +Each has a direct mt76 recipe. Estimates are the C-library side only. + +1. **`StartBeacon` — load the beacon + arm the MAC beacon function (~70 LOC).** + - Add two register defines our `regs.h` lacks: `MT_BEACON_BASE` (0xc000) and + `MT_BCN_OFFSET(n)` (0x041c + n·4). Everything else is present. + - Program the 5 USB beacon slots: `slot_size = (8192/5) & ~63 = 1600`, + offsets via `MT_BCN_OFFSET` — mt76 `mt76x02_set_beacon_offsets` + (`mt76x02_beacon.c:10`, `N_BCN_SLOTS=5` in `mt76x02_usb_core.c:126`). + - Write `[TXWI][beacon MPDU]` into `MT_BEACON_BASE` with `mt_wr_copy()` — + mt76 `mt76x02_write_beacon`/`mt76x02_mac_set_beacon` + (`mt76x02_beacon.c:24,54`). `mt_tx_build()` already emits that shape. + - Enable: set `MT_BEACON_TIME_CFG` `BEACON_TX | TBTT_EN | TIMER_EN` with + `INTVAL = interval_tu` — mt76 `mt76x02_mac_set_beacon_enable` + (`mt76x02_beacon.c:69`). Point `MT_MAC_BSSID_*` at the AP BSSID, **unicast** + (`0x02…`) — `docs/ap-mode.md`'s hardest-won finding: an I/G-set BSSID makes + the station drop auth before it reaches the air. + - 802.11 sequence numbering: let the MAC number it (the HW-seq path mt76 + uses for beacons) or number it per update in software; `beacon_wire_check` + expects +1 per beacon. + - **Trap — a silent no-transmit.** `MT_BCN_BYPASS_MASK` (0x108c) is + **inverted**. `0xffff`, the value `mt76x02_init_beacon_config()` writes, is + "suppress every slot" — the guard mt76 raises *during* an update ("prevent + corrupt transmissions during update"). A slot only airs once its bit is + **cleared**, which mt76 does after loading the page: + `0xff00 | ~(0xff00 >> beacon_data_count)` (`mt76x02_usb_core.c:223`, + identically `mt76x02_mmio.c:43`); for one beacon in slot 0 that is + `0xffffff7f`. Leave it at `0xffff` and the beacon timer runs, the TSF + advances and every register reads correct — while nothing reaches the air. + - **Trap — the beacon TXWI needs two extra bits.** `MT_TXWI_FLAGS_TS` (BIT 3; + MAC inserts the TSF timestamp) and `MT_TXWI_ACK_CTL_NSEQ` (MAC assigns the + sequence number), exactly as `mt76x02_mac_write_txwi()` sets them for + beacon/probe-resp subtypes. Without them the beacon airs with a frozen + timestamp and a constant sequence number. + - **Trap — `regs.h` had the MBSS masks two bits high.** `MBSS_MODE`, + `MBEACON_N` and `MBSS_LOCAL_BIT` were transcribed as 19:18 / 22:20 / 23 + instead of mt76's 17:16 / 20:18 / 21. Harmless while the port only + injected (only `mac_setaddr` uses them, and beacons were never generated), + but it programmed MBSS_MODE=4 (invalid) and MBEACON_N=15. Corrected; + `MT_MAC_BSSID_DW1` now reads `0x003f____`, matching mt76 bit for bit. + +2. **`StopBeacon` (~10 LOC).** Clear `BEACON_TX | TBTT_EN | TIMER_EN`. Note the + `IRadio` contract: the chip beacons autonomously, so a session that ends + without a power-cycle **must** call this or the beacon contaminates the next + run (`src/IRadio.h:407-413`). + +3. **Per-station / group key install (~50 LOC).** `MT_WCID_KEY(idx)` + + `MT_WCID_ATTR` PKEY_MODE/PAIRWISE for pairwise, `MT_SKEY` + `MT_SKEY_MODE` + for the GTK — mt76 `mt76x02_mac_wcid_set_key` / `mac_shared_key_setup` + (`mt76x02_mac.c`). The cipher enum (`MT76X02_CIPHER_*`) is small. This buys + **hardware CCMP**; the software CCMP in `ap_wpa2.cpp` still works as the + fallback/portable path. + +4. **AP RX filter — one line, not ~15 LOC.** Measured: the managed default + `0x00015f97` already leaves `OTHER_BSS`, `BCAST` and `MCAST` **undropped**, + so a probe request with a wildcard BSSID and auth addressed to us both + arrive unchanged; mt76 clears `OTHER_BSS` for every mode too + (`mt76x02_configure_filter`), and defines no AP-specific filter. The single + change an AP wants is clearing `DUP` (`0x00015f17`) so a station's + retransmission stays visible — dropping duplicates hides exactly the + retry evidence that tells you whether your ACKs are landing. + +5. **`SetAckResponder` — covered by address match. OPEN ITEM NOW RESOLVED.** + On MT the MAC auto-ACKs frames matching the programmed `MT_MAC_ADDR`, so + pointing it at the BSSID (done in `mac_setaddr`) is the ACK responder. + Confirmed on hardware: **there is no AP op-mode/net-type register on this + part.** mt76 sets none — its only AP-specific work in `mt76x02_sta_add` is a + *software* PS flag (`MT_WCID_FLAG_CHECK_PS`) plus a GTK restriction — and a + real station's auth arrived at **retry=0** with nothing but address match + + beacon enable. Address match + beacon *is* the AP. + - **But one register the gap analysis missed:** `mac_setaddr()` zeroes all + eight APC BSSID slots, which is right for an injector and wrong for an AP. + The MAC matches the BSS against `MT_MAC_APC_BSSID_L/H(idx)` for the slot + the MBSS index selects, so an AP must publish its BSSID there — + `mt76x02_mac_set_bssid()` (`mt76x02_mac.c`), 4 lines. Without it the MAC + matches nothing for the BSS. + - The simplest consistent choice is to make the AP BSSID the device's own + MAC: `mac_setaddr()` already programs it into `MT_MAC_ADDR` (what the MAC + ACKs against) and `MT_MAC_BSSID`, so only the APC slot is left to write. + Advertising any *other* BSSID in the beacon leaves a station addressing + auth to an address the MAC does not answer for. + +6. **`UpdateBeaconPayload` (~10 LOC, optional).** Re-write the reserved page in + place. Only needed for dynamic beacon content (below). + +### The two integration layers + +- **The mt7612u C library** gains items 1–4 above (~150 LOC, all with recipes). +- **`Mt7612uRadio`** (the wrapper from the integration PR) exposes them as + `StartBeacon`/`StopBeacon`/`UpdateBeaconPayload`/`SetAckResponder` over the C + ABI, so the **existing** C++ AP harnesses in `tests/` run unchanged. No AP + logic is written — it already exists. + +## Limitations and shortfalls of a userspace AP on MediaTek — and workarounds + +1. **Power-save / TIM is the real fight — USB has no pre-TBTT interrupt.** + A dynamic beacon (TIM bitmap, buffered multicast/broadcast) must be rewritten + just before each TBTT. The kernel fakes the interrupt with an hrtimer firing + 8 ms pre-TBTT plus a high-priority workqueue that refills up to `N_BCN_SLOTS` + buffered frames (`mt76x02u_pre_tbtt_work`, `mt76x02_usb_core.c:128‑217`). In + userspace that is a timer thread racing a 1–2 ms USB write, and a dense + 25 TU beacon (needed so a scanning supplicant catches the AP) fires it ~40×/s. + - **Workaround (recommended for FPV/video-return):** a **static** beacon is + free — the MAC auto-beacons from the reserved page with no host + involvement. Advertise DTIM=1, no buffering, and do not support + power-saving clients (the return-video use case has always-on clients). + Then `StartBeacon` alone suffices and the pre-TBTT machinery is never + needed. + - **If PS clients are required:** port the hrtimer+worker as a userspace + timer thread that calls `UpdateBeaconPayload` pre-TBTT. This is the one + place the USB-userspace shape genuinely fights the protocol; budget for + jitter and missed updates. + +2. **BlockAck RX reordering is software.** TX aggregation is hardware, but if a + client sends A-MPDU the AP must own a reorder buffer. + - **Workaround:** negotiate **no** BlockAck (decline ADDBA) — clients fall + back to non-aggregated data, lower uplink throughput but correct. Implement + a reorder window only if client→AP throughput demands it. + +3. **No firmware rate control.** `txwi.rate` airs verbatim (proven — there is no + rate LUT), so the AP picks every client's TX rate in host software. + - **Workaround:** a fixed rate, or a simple picker off the per-chain RSSI the + RX path already reports (`rssi[0..1]`). Adequate for a handful of clients. + +4. **Multiple concurrent clients.** The WCID table is 256 entries and SKEY is + per-BSS in hardware, so the silicon supports many clients; the current AP + harness handles one (`docs/ap-mode.md` scope). Extending is **harness** work + (a client table, per-client PTK), not driver work. + +5. **Out of scope, standard AP-stack breadth.** WMM/QoS EDCA parameter sets, + DFS/radar on 5 GHz DFS channels, 802.11w management-frame protection, band + steering, and a real DHCP pool. None are MT-specific gaps; they are AP-stack + features the harness does not implement. + +6. **Management-frame timing is fine.** Probe/auth/assoc responses are tens-of-ms + tolerant and the userspace RX→TX round-trip is a few ms, proven on the + Realtek stack (`docs/ap-mode.md`); only SIFS-timed ACK must be hardware, and + it is. + +**Net:** an open or WPA2-PSK AP serving a few always-on clients is very +achievable and *easier* on MT7612U than on Realtek (hardware CCMP + GTK). The +static-beacon path avoids the one hard USB limitation entirely. Power-save +clients and BlockAck reordering are where a userspace MT AP stops being worth +it, and both have clean "don't support it" workarounds for the return-video +use case. + +## End-to-end verification (reuse, don't rebuild) + +devourer's existing AP checks are backend-agnostic and become the acceptance +suite once the MT backend implements the primitives: + +- `tests/beacon_wire_check.cpp` — beacon frame control, +1 seq per beacon, live TSF. +- `tests/beacon_kernel_scan.sh` — a real `rtw88` station's `iw scan` lists the AP. +- `tests/probe_responder.cpp` — active-scan probe response, no beacon. +- `tests/ap_responder.cpp` + `tests/ap_ping_demo.sh` — open assoc → DHCP lease → + ping 0% loss. +- `tests/ap_wpa2.cpp` + `tests/ap_wpa2_demo.sh` — WPA2 4-way → encrypted DHCP → + encrypted ping (here, exercise the **hardware** CCMP path). + +Success = a real Linux station associates and passes IP traffic against the +MT7612U backend, open and WPA2-PSK, on both 2.4 and 5 GHz, with the static +beacon. diff --git a/src/mt7612u/Mt7612uRadio.cpp b/src/mt7612u/Mt7612uRadio.cpp index 4e3cd222..37307d6b 100644 --- a/src/mt7612u/Mt7612uRadio.cpp +++ b/src/mt7612u/Mt7612uRadio.cpp @@ -634,6 +634,25 @@ void Mt7612uRadio::Stop() { StopRxLoop(); } catch (...) { } + /* Before anything else lets go of the device: the MAC beacons AUTONOMOUSLY + * once armed, so a beacon that outlives this object keeps airing until the + * adapter is power-cycled and contaminates whatever runs next on that + * channel. Bench-bitten on the Realtek side, and the bring-up gate silences + * its beacon on every exit path for the same reason. */ + try { + /* Retried, because the comment in StopBeacon promises one and because a + * beacon that survives this call survives the process: the MAC keeps + * airing it until the adapter is power-cycled. Three attempts, then say so + * at error level rather than closing the device in silence. */ + bool silenced = false; + for (int attempt = 0; attempt < 3 && !silenced; ++attempt) + silenced = StopBeacon(); + if (!silenced && _beacon_active) + _logger->error("MT7612U: closing the device with a beacon still armed - " + "the MAC will keep airing it until the adapter is " + "power-cycled"); + } catch (...) { + } stop_tick(); /* joins; must not run with _mu held */ /* Take the device out under _mu, then close it OUTSIDE - mt7612u_close() @@ -841,6 +860,98 @@ void Mt7612uRadio::ClearAckResponder() { mt7612u_clear_ack_responder(_dev); } +/* The beacon plane. Thin on purpose: the sequence these wrap is the one the + * bring-up harness's Stage A and Stage B gates run, device-verified on + * 2026-09-08 - beacon on air on both bands, hardware TSF and sequence, and a + * real station's auth arriving at retry=0, which is the auto-ACK. Putting it + * behind IRadio is what lets devourer's existing backend-agnostic AP + * harnesses (tests/ap_responder.cpp, tests/ap_wpa2.cpp - both already take an + * IRadio*) drive this part with no MediaTek-specific code in them. + * + * Under _mu with the rest of the control plane: every one of these is a + * register write, and the 1 Hz tick is issuing MCU traffic on its own thread. */ +bool Mt7612uRadio::StartBeacon(const uint8_t *beacon, size_t len, + int interval_tu) { + std::lock_guard lock(_mu); + if (!_dev || !beacon || len == 0 || interval_tu <= 0) + return false; + /* Three outcomes, not two, because a failed re-arm has to say whether the + * PREVIOUS beacon is still on the air: + * + * 0 armed + * -1 refused before the hardware was touched - whatever was airing still + * is, so the flag must NOT be cleared. Clearing it here was a way to + * orphan a live beacon: update, stop and the destructor would all then + * treat it as inactive and nobody would ever silence it. + * -2 failed after the engine was disarmed, and the library unwound the + * rest - so nothing is airing and the flag is false. */ + const int rc = mt7612u_beacon_start(_dev, beacon, len, + static_cast(interval_tu)); + if (rc == -2) + _beacon_active = false; + if (rc != 0) + return false; + _beacon_active = true; + _logger->info("MT7612U beaconing every {} TU", interval_tu); + return true; +} + +bool Mt7612uRadio::UpdateBeaconPayload(const uint8_t *beacon, size_t len) { + std::lock_guard lock(_mu); + /* "Requires an active StartBeacon; returns false otherwise" - and without + * the guard this would load a beacon into a disarmed engine and report + * success for something that never airs. */ + if (!_dev || !_beacon_active || !beacon || len == 0) + return false; + return mt7612u_beacon_update(_dev, beacon, len) == 0; +} + +/* The beacon-steer trio. Not implemented, and REFUSING QUIETLY IS THE PROBLEM: + * IRadio's default returns 0, which means "applied a 0 us shift" and is + * indistinguishable from "this backend cannot steer". tests/beacon_steer_check + * and the PTP harnesses run against any backend and would read a clean zero. + * Every other unsupported knob here says so out loud; these were the exception. + * + * Steering needs a pre-TBTT interrupt to re-time against, which this static + * reserved-page path does not have - mt76's own steering lives in + * mt76x02u_pre_tbtt_work(), a path this port does not run. */ +int32_t Mt7612uRadio::AdjustBeaconTiming(int32_t microseconds) { + (void)microseconds; + _logger->error("MT7612U: AdjustBeaconTiming is not implemented - the static " + "reserved-page beacon has no pre-TBTT hook to steer against"); + return 0; +} + +int32_t Mt7612uRadio::AdjustBeaconTimingFine(int32_t microseconds) { + (void)microseconds; + _logger->error("MT7612U: AdjustBeaconTimingFine is not implemented"); + return 0; +} + +int32_t Mt7612uRadio::PinBeaconTbtt(int32_t offset_us) { + (void)offset_us; + _logger->error("MT7612U: PinBeaconTbtt is not implemented"); + return 0; +} + +bool Mt7612uRadio::StopBeacon() { + std::lock_guard lock(_mu); + if (!_dev || !_beacon_active) + return false; + if (mt7612u_beacon_stop(_dev) != 0) { + /* Deliberately still active. A caller retrying after a transient USB + * stall must not be told "already stopped" - false means "no beacon was + * active" in this interface, and reading a failed stop as that walks away + * from a beacon the MAC is still airing. Stop() calls this inside a + * try/catch, so a retry there costs nothing. */ + _logger->error("MT7612U beacon stop FAILED - the MAC is still airing it; " + "retry, or power-cycle the adapter"); + return false; /* _beacon_active deliberately left set: see above */ + } + _beacon_active = false; + return true; +} + /* The absolute dBm the actuator should carry: the base plus whatever offset is * live, clamped to the part's 0-30 range. One place, so the base setter, the * offset setter and the bring-up replay cannot drift apart. */ @@ -947,7 +1058,11 @@ devourer::AdapterCaps Mt7612uRadio::GetAdapterCaps() { c.ldpc_rx_flag = true; /* the RXWI carries the per-frame LDPC bit */ c.per_chain_rssi = true; c.hw_rx_timestamp = false; /* the RXWI TSF field is not parsed */ - c.hw_beacon_txtsf = false; /* no hardware beacon function ported */ + /* The MAC inserts the live 64-bit TSF into the beacon it auto-transmits; + * measured at 102400 us per beacon, exactly 100 TU (docs/mt7612u-ap-mode.md). + * True since the beacon plane landed - it read false while the function it + * describes sat three hundred lines above. */ + c.hw_beacon_txtsf = true; /* Measured on air: 0 frames at the stimulus radio unarmed, 3500+ armed. */ c.ack_responder_ok = true; /* Unmeasured, so false rather than optimistic - nothing here drives the diff --git a/src/mt7612u/Mt7612uRadio.h b/src/mt7612u/Mt7612uRadio.h index 1e25b917..c0ede18b 100644 --- a/src/mt7612u/Mt7612uRadio.h +++ b/src/mt7612u/Mt7612uRadio.h @@ -100,6 +100,13 @@ class Mt7612uRadio : public IRadio { void WriteTsf(uint64_t tsf) override; devourer::TxStats GetTxStats() override; bool SetAckResponder(const devourer::MacAddr &mac) override; + bool StartBeacon(const uint8_t *beacon, size_t len, int interval_tu) override; + bool UpdateBeaconPayload(const uint8_t *beacon, size_t len) override; + bool StopBeacon() override; + /* Refuse loudly rather than report a 0 us shift that was never applied. */ + int32_t AdjustBeaconTiming(int32_t microseconds) override; + int32_t AdjustBeaconTimingFine(int32_t microseconds) override; + int32_t PinBeaconTbtt(int32_t offset_us) override; void ClearAckResponder() override; private: @@ -160,6 +167,12 @@ class Mt7612uRadio : public IRadio { std::condition_variable _tick_cv; bool _tick_stop = false; + /* StopBeacon/UpdateBeaconPayload are documented to return false when no + * beacon is active, and the MAC keeps beaconing after the host process dies, + * so the destructor needs to know too. Guarded by _mu like the rest of the + * control plane. */ + bool _beacon_active = false; + int _txpwr_dbm = 20; /* the absolute dBm limit mt7612u_set_txpower takes */ int _txpwr_offset_qdb = 0; /* sticky, folded onto _txpwr_dbm */ }; diff --git a/src/mt7612u/beacon.cpp b/src/mt7612u/beacon.cpp new file mode 100644 index 00000000..85143437 --- /dev/null +++ b/src/mt7612u/beacon.cpp @@ -0,0 +1,482 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * Static reserved-page beacon. + * + * The MT76 MAC auto-transmits a beacon written to MT_BEACON_BASE at every + * TBTT, filling the TSF timestamp and the 802.11 sequence number in hardware + * (MT_TXWI_FLAGS_TS + MT_TXWI_ACK_CTL_NSEQ, requested via MT_TXOPT_BEACON). + * So an always-on AP needs no host involvement per beacon: no pre-TBTT timer, + * no worker thread. That is the one USB-userspace limitation the AP design + * engineers out - see docs/mt7612u-ap-mode.md. + * + * Dynamic beacon content (a live TIM bitmap for power-saving clients) is NOT + * covered here; it would need the pre-TBTT machinery mt76 runs on PCIe/USB. + * + * Ported from mt76/mt76x02_beacon.c and mt76x02_usb_core.c @ be5ce79. + * Copyright (C) 2016 Felix Fietkau, (C) 2018 Lorenzo Bianconi / Stanislaw Gruszka. + */ +#include +#include "internal.h" + +/* mt76x02u: 5 USB beacon slots, each (8192 / 5) & ~63 = 1600 bytes. The 8 kB + * reserved page is shared with PS-buffered frames upstream; we use slot 0. */ +#define MT_BCN_NSLOTS 5 +#define MT_BCN_SLOT_SIZE ((8192 / MT_BCN_NSLOTS) & ~63) + +/* mt76x02_set_beacon_offsets(): each slot's (byte offset / 64) is packed into + * MT_BCN_OFFSET, four slots to a 32-bit register. */ +static void mt_beacon_set_offsets(struct mt7612u_dev *d) +{ + uint32_t regs[4] = { 0 }; + int i; + + for (i = 0; i < MT_BCN_NSLOTS; i++) { + uint32_t val = (uint32_t)i * MT_BCN_SLOT_SIZE; + + regs[i / 4] |= (val / 64) << (8 * (i % 4)); + } + for (i = 0; i < 4; i++) + mt_wr(d, MT_BCN_OFFSET(i), regs[i]); +} + +/* + * mt76x02_init_beacon_config(): quiet the beacon engine, select sync mode, + * suppress every beacon slot while the page is being set up, and lay out the + * slot offsets. Run once before the first beacon is written. The address + * programming (BSSID, MBSS mode, per-slot beacon count) is done by + * mac_setaddr() at init. BCN_BYPASS_MASK=0xffff suppresses all slots here; + * mt_beacon_write() clears the bit for the slot it loads so that one airs. + */ +void mt_beacon_init(struct mt7612u_dev *d) +{ + mt_clear(d, MT_BEACON_TIME_CFG, + MT_BEACON_TIME_CFG_TIMER_EN | MT_BEACON_TIME_CFG_TBTT_EN | + MT_BEACON_TIME_CFG_BEACON_TX); + mt_set(d, MT_BEACON_TIME_CFG, MT_BEACON_TIME_CFG_SYNC_MODE); + mt_wr(d, MT_BCN_BYPASS_MASK, 0xffff); /* suppress all while we set up */ + mt_beacon_set_offsets(d); +} + +/* + * mt76x02_mac_set_beacon(): write [TXWI][beacon MPDU] into slot 0. + * + * mt_tx_build() emits [TXINFO 4][TXWI 20][802.11][pad], which is the shape the + * TX queue wants; the reserved page wants no TXINFO and no trailing zero word, + * so skip the first 4 bytes and copy from the TXWI onward. A beacon header is + * 24 bytes (4-aligned), so mt_tx_build() inserts no interior L2 pad and the + * copied region is exactly [TXWI][MPDU] rounded up to a word. + */ +int mt_beacon_write(struct mt7612u_dev *d, const void *frame, size_t len, + const struct mt7612u_tx_rate *rate) +{ + uint8_t buf[MT_BCN_SLOT_SIZE]; + int total; + + /* The slot must hold the TXWI and the DMA header as well as the body; + * mt_tx_build() enforces the tighter bound anyway, but from here the + * error names the beacon rather than reporting a bad frame length. */ + if (len + MT_TXWI_LEN + MT_DMA_HDR_LEN > MT_BCN_SLOT_SIZE) { + ERR("beacon %zu B + TXWI exceeds the %d B slot", len, + (int)MT_BCN_SLOT_SIZE); + return -1; + } + total = mt_tx_build(d, buf, sizeof buf, frame, len, rate, 0xff, + MT_TXOPT_BEACON, 0, 0); + if (total < 0) + return -1; + + /* Checked by the io_err delta, because mt_wr_copy() returns void and + * gives up mid-loop on the first failed vendor request - leaving a HALF + * WRITTEN beacon in the page, which then airs. That is worse than no + * beacon, and the paragraph below already argues why USB writes here get + * checked when mt76's MMIO ones do not. */ + { + const unsigned before = mt_io_errors(d); + mt_wr_copy(d, MT_BEACON_BASE, buf + MT_DMA_HDR_LEN, + total - MT_DMA_HDR_LEN); + if (mt_io_errors(d) != before) { + ERR("beacon: the reserved-page copy failed part way"); + return -1; + } + } + + /* + * Unsuppress the slot just written. BCN_BYPASS_MASK is inverted: a set + * bit suppresses that slot (mt_beacon_init() set all 16). mt76 clears the + * low N bits down from bit 7 for N written beacons - 0xff00 | ~(0xff00 >> + * beacon_data_count) in mt76x02u_pre_tbtt_work() - and the static path + * writes exactly one (slot 0), so N = 1. Without this the beacon never + * airs even though the TSF and beacon timer run. + * + * Checked, unlike mt_wr(): this single write decides whether the beacon + * airs at all, and mt76 can leave it unchecked because MMIO cannot fail + * while USB can - the same argument mt_ap_set_bssid() makes below. A + * silent failure here is an AP that beacons nothing while every other + * step reports success. + */ + return mt_wr_chk(d, MT_BCN_BYPASS_MASK, 0xff00u | ~(0xff00u >> 1)); +} + +/* + * mt76x02_mac_set_bssid(): the per-BSS address the MAC matches receptions + * against. mac_setaddr() zeroes all eight APC slots at init, which is right for + * an injector; an AP must publish its own BSSID in the slot its MBSS index + * selects (0 for a single BSS) or the MAC matches nothing for the BSS - a + * station's auth is then neither accepted nor auto-ACKed, and it retries + * forever. Called by the AP path; the injector never needs it. + */ +int mt_ap_set_bssid(struct mt7612u_dev *d, uint8_t idx, const uint8_t *addr) +{ + uint32_t lo = (uint32_t)addr[0] | ((uint32_t)addr[1] << 8) | + ((uint32_t)addr[2] << 16) | ((uint32_t)addr[3] << 24); + uint32_t hi = (uint32_t)addr[4] | ((uint32_t)addr[5] << 8); + + idx &= 7; + if (mt_wr_chk(d, MT_MAC_APC_BSSID_L(idx), lo)) + return -1; + /* mt_rmw() skips the write entirely when its read half fails, so an + * unchecked call can leave bytes 4-5 zero - a half-programmed BSSID that + * matches nothing while the L half still reads back correct. mt76 returns + * void here because MMIO cannot fail; USB can. */ + return mt_rmw(d, MT_MAC_APC_BSSID_H(idx), MT_MAC_APC_BSSID_H_ADDR, hi); +} + +/* + * mt76x02_mac_set_beacon_enable(), static path. No pre-TBTT timer: the MAC + * transmits the reserved-page beacon on its own once BEACON_TX|TBTT_EN|TIMER_EN + * are set. interval_tu is the beacon interval in TU (1024 us); the register + * counts in 1/16 TU, so it is shifted left by 4 (mt76x02_bss_info_changed). + */ +int mt_beacon_set_enable(struct mt7612u_dev *d, int on, unsigned interval_tu) +{ + const uint32_t bits = MT_BEACON_TIME_CFG_BEACON_TX | + MT_BEACON_TIME_CFG_TBTT_EN | + MT_BEACON_TIME_CFG_TIMER_EN; + + if (on) { + /* INTVAL is 16 bits of 1/16-TU, so the interval caps at 4095 TU. */ + if (interval_tu == 0 || interval_tu > 0xffffu / 16) { + ERR("beacon interval %u TU out of range (1..4095)", interval_tu); + return -1; + } + mt_rmw(d, MT_BEACON_TIME_CFG, MT_BEACON_TIME_CFG_INTVAL, + FIELD_PREP(MT_BEACON_TIME_CFG_INTVAL, interval_tu << 4)); + mt_set(d, MT_BEACON_TIME_CFG, bits); + } else { + mt_clear(d, MT_BEACON_TIME_CFG, bits); + } + return 0; +} + +/* --- public ABI --------------------------------------------------------- + * + * The three calls devourer's IRadio beacon surface maps onto. Everything they + * do is the sequence bringup's Stage A and Stage B gates run and that was + * device-verified on 2026-09-08 (docs/mt7612u-ap-mode.md); this is that + * sequence behind the public header, so a consumer does not have to reach into + * internal.h to be an AP. + */ + +/* Split a radiotap-framed buffer into rate + MPDU, exactly as + * mt7612u_send_packet() does. A bare MPDU (no radiotap) is not an error here - + * IRadio's contract strips the header "if present" - and takes the rate a + * beacon wants: OFDM 6 Mbps, the basic rate every station must decode. */ +static int beacon_split(const void *buf, size_t len, const uint8_t **mpdu, + size_t *mpdu_len, struct mt7612u_tx_rate *r) +{ + const uint8_t *p = (const uint8_t *)buf; + int rlen; + + if (!p || len == 0) return -1; + + /* ZEROED FIRST. The bare-MPDU branch below sets five of this struct's + * nine fields, and sgi/ldpc/stbc go straight into the 16-bit rate word + * the MAC transmits verbatim, while power_adj short-circuits the derived + * per-rate TX power in mt_tx_build(). Left indeterminate, a bare-MPDU + * beacon - which is what tests/ap_responder.cpp hands us - airs with + * whatever was on the stack. The radiotap branch only escaped this + * because mt_radiotap_parse() memsets its output. */ + *r = mt7612u_tx_rate{}; + + /* + * Which shape is this? An 802.11 beacon's first byte is its frame control, + * 0x80 - never 0. A radiotap header's first byte is its version, which must + * be 0. So byte 0 decides, and each shape is then held to its own rules + * rather than being allowed to fall back to the other: + * + * mt_radiotap_parse() returns 0 for BOTH "not a radiotap header" and "is + * one and it is malformed" (bad version, bad length, truncated present + * map), and negative for "declared a field past its own length". Treating + * any of those as a bare MPDU parses radiotap bytes as an 802.11 header and + * loads them into the beacon page - which then airs. + */ + if (p[0] == 0) { + rlen = mt_radiotap_parse(p, len, r); + if (rlen <= 0) { + ERR("beacon: radiotap header is malformed"); + return -1; + } + if ((size_t)rlen >= len) { + ERR("beacon: %d B of radiotap and no frame after it", rlen); + return -1; + } + *mpdu = p + rlen; + *mpdu_len = len - (size_t)rlen; + } else { + /* A bare MPDU. OFDM 6 Mbps is the basic rate every station must + * decode, which is what a beacon wants. */ + r->phy = MT7612U_PHY_OFDM; + r->mcs = 0; + r->nss = 1; + r->bw = MT7612U_BW_20; + *mpdu = p; + *mpdu_len = len; + } + + /* Unconditionally, whatever the caller's radiotap said: a beacon is + * broadcast, and mt_tx_build() turns a cleared no_ack into + * MT_TXWI_ACK_CTL_REQ - an ACK request on a frame no one may ACK. Both + * bring-up gates hardcode this; the ABI must not be weaker. */ + r->no_ack = 1; + + /* addr3 lives at offset 16, so anything shorter has no BSSID to publish. */ + if (*mpdu_len < 24) { + ERR("beacon: %zu B is too short for an 802.11 header", *mpdu_len); + return -1; + } + /* mt_beacon_write() documents that it relies on a 24-byte, 4-aligned + * header so mt_tx_build() inserts no interior L2 pad. A QoS-data or + * 4-address frame (26 or 30) passes every check above and would land in + * the reserved page as [TXWI][hdr][2 pad][body] - a layout that function + * is written not to expect. Enforce what it assumes. */ + if (mt_hdrlen_from_fc(*mpdu) != 24) { + ERR("beacon: header is %d B, not the 24 a beacon has - the reserved " + "page needs an unpadded [TXWI][MPDU]", + mt_hdrlen_from_fc(*mpdu)); + return -1; + } + return 0; +} + +/* + * The MBSS base address: MT_MAC_BSSID_DW0/DW1's address halves, leaving + * MBSS_MODE / MBEACON_N / LOCAL_BIT alone. + * + * This is the half of "retarget the identity" that mt76 does and devourer's + * ACK responder does not. mt76x02_mac_setaddr() moves mphy.macaddr, + * MT_MAC_ADDR and MT_MAC_BSSID together, and the whole per-BSS index + * derivation is written against that invariant. Move only MT_MAC_ADDR - which + * is all mt7612u_set_ack_responder() does, and all this function used to do - + * and the MBSS base is still the factory address, so the hardware derives the + * BSS index from a different address than the host thinks it does. That is + * silent: the AP beacons perfectly and matches nobody. + */ +static int mt_mac_set_bss_base(struct mt7612u_dev *d, const uint8_t *a) +{ + const uint32_t dw0 = (uint32_t)a[0] | ((uint32_t)a[1] << 8) | + ((uint32_t)a[2] << 16) | ((uint32_t)a[3] << 24); + const uint32_t dw1 = (uint32_t)a[4] | ((uint32_t)a[5] << 8); + + if (mt_wr_chk(d, MT_MAC_BSSID_DW0, dw0)) + return -1; + return mt_rmw(d, MT_MAC_BSSID_DW1, MT_MAC_BSSID_DW1_ADDR, dw1); +} + +/* Put the identity back if THIS call was what moved it. Shared by every + * failure path in beacon_start and by beacon_stop. */ +static void unwind_identity(struct mt7612u_dev *d, int took) +{ + if (!took) + return; + mt7612u_clear_ack_responder(d); + mt_mac_set_bss_base(d, d->macaddr); + d->beacon_took_identity = 0; +} + +int mt7612u_beacon_start(struct mt7612u_dev *dev, const void *buf, size_t len, + unsigned interval_tu) +{ + struct mt7612u_tx_rate rate; + const uint8_t *mpdu = NULL; + size_t mpdu_len = 0; + const uint8_t *ta, *bssid; + uint8_t idx; + unsigned before; + int took = 0; + + if (!dev) return -1; + if (beacon_split(buf, len, &mpdu, &mpdu_len, &rate)) return -1; + + ta = mpdu + 10; /* addr2 - the transmitter, i.e. the port identity */ + bssid = mpdu + 16; /* addr3 */ + + if (ta[0] & 0x01) { + ERR("beacon addr2 must be unicast; a station cannot unicast-auth to " + "a multicast BSSID"); + return -1; + } + + /* + * mt76x02_add_interface(): the port identity FOLLOWS the interface + * address. IRadio says the same thing - "addr2/addr3 set the port + * MAC/BSSID" - and devourer's AP harnesses rely on it ("MACID = BSSID, + * set by StartBeacon", tests/ap_responder.cpp). Without this the MAC + * would keep ACKing for the adapter's factory MAC while beaconing a + * different BSSID, so a station's auth is never acknowledged and it + * retries until it gives up. + * + * mt7612u_set_ack_responder() is that register write, and it saves the + * factory identity so ClearAckResponder() can put it back. The two share + * one identity and one save slot by construction - there is only one + * MT_MAC_ADDR on this part - so a caller that arms a responder AND + * beacons is setting the same thing twice, and the restore is whichever + * of the two runs last. + */ + /* Unconditional, and BOTH registers. Unconditional because the old + * `ta != dev->macaddr` guard compared against the FACTORY address - + * dev->macaddr is written once, from the EEPROM, and nothing moves it - + * so a caller who had armed an ACK responder and then beaconed as the + * factory MAC kept the responder's address in MT_MAC_ADDR and ACKed for + * the wrong station all session. The retarget is idempotent and costs two + * EP0 writes; there is nothing to save by skipping it. + * + * Both registers because the index below is derived from the MBSS base, + * not from MT_MAC_ADDR. */ + { + const int was_taken = dev->ack_saved; + if (mt7612u_set_ack_responder(dev, ta)) + return -1; + if (mt_mac_set_bss_base(dev, ta)) + return -1; + /* Only claim ownership if nobody else already held the identity: a + * caller who armed a responder first owns the saved factory address, + * and restoring it on beacon stop would silently disarm them. */ + if (!was_taken) { + dev->beacon_took_identity = 1; + took = 1; + } + } + + /* + * The APC slot the hardware will match this BSS in. Under MBSS_MODE=3 the + * index comes from the address bits, and mt76 computes + * idx = 1 + (((macaddr[0] ^ addr[0]) >> 2) & 7) + * for a locally-administered address, 0 otherwise (mt76x02_util.c:310). + * mt76 runs that AFTER mt76x02_mac_setaddr(), so its macaddr IS addr and + * the XOR is zero, collapsing the expression to 1. mt_mac_set_bss_base() + * above is what makes the same thing true here - without it the base + * stays the factory address, the hardware derives 1 + ((factory[0] ^ + * ta[0]) >> 2 & 7), and this constant is right only for the adapters + * where that happens to be 1. Getting it wrong is silent: the AP beacons + * perfectly and acknowledges nobody. + */ + idx = (ta[0] & 0x02) ? 1 : 0; + + /* + * From here the hardware is being changed, so every exit unwinds. Two + * things are being unwound, and they are different: + * + * - the identity, if THIS call took it. Leaving it retargeted after a + * failed start means the adapter answers for a BSS that does not exist. + * - the beacon engine, once mt_beacon_init() has disarmed it. That is why + * a failure at or after that point returns -2 and one before it returns + * -1: only the caller can know whether a PREVIOUS beacon is still on the + * air, and -1 means "nothing was touched, whatever was airing still is". + * + * And the io_err delta, because mt_beacon_set_enable()'s arming path is + * mt_rmw()/mt_set() - which report their read halves, not their writes - so + * without it a failed EP0 transfer returns 0 and the caller reports an AP + * that is ready and airing nothing. + */ + before = mt_io_errors(dev); + + if (mt_ap_set_bssid(dev, idx, bssid)) + goto fail_pre; + mt_beacon_init(dev); + if (mt_beacon_write(dev, mpdu, mpdu_len, &rate)) + goto fail_post; + if (mt_beacon_set_enable(dev, 1, interval_tu)) + goto fail_post; + if (mt_io_errors(dev) != before) { + ERR("beacon: a USB transfer failed while arming"); + goto fail_post; + } + return 0; + +fail_post: + mt_beacon_set_enable(dev, 0, 0); + unwind_identity(dev, took); + return -2; +fail_pre: + unwind_identity(dev, took); + return -1; +} + +int mt7612u_beacon_update(struct mt7612u_dev *dev, const void *buf, size_t len) +{ + struct mt7612u_tx_rate rate; + const uint8_t *mpdu = NULL; + size_t mpdu_len = 0; + + if (!dev) return -1; + if (beacon_split(buf, len, &mpdu, &mpdu_len, &rate)) return -1; + + /* Suppress the slot for the duration of the copy. mt_wr_copy() spans many + * 64-byte EP0 transactions, so a TBTT landing mid-copy would air a TORN + * beacon - leading bytes new, trailing bytes old. mt76 brackets the same + * write for the same reason ("Prevent corrupt transmissions during + * update", mt76x02_usb_core.c). mt_beacon_write() lowers the guard again + * on its way out, which is why this is the only half needed here. + * + * Still no mt_beacon_init() and no set_enable(): the engine is armed, and + * re-initialising it would clear the timer bits mid-flight. */ + if (mt_wr_chk(dev, MT_BCN_BYPASS_MASK, 0xffff)) { + /* Checked: if the guard never lands, the copy below runs against a + * LIVE slot and a TBTT mid-copy airs a torn beacon - which is the one + * outcome this bracket exists to prevent, so failing to raise it must + * not be the quiet path. */ + ERR("beacon: could not suppress the slot for an in-place update"); + return -1; + } + return mt_beacon_write(dev, mpdu, mpdu_len, &rate); +} + +int mt7612u_beacon_stop(struct mt7612u_dev *dev) +{ + static const uint8_t zero[6] = { 0 }; + unsigned before; + int rc; + + if (!dev) return -1; + + /* The io_err delta is what makes a failed stop VISIBLE. mt_beacon_set_enable's + * off path is mt_clear() -> mt_rmw(), which reports only its READ half, and + * mt_ap_set_bssid()'s writes are checked but their returns were dropped. So + * this function used to be incapable of returning non-zero, which made + * Mt7612uRadio::StopBeacon's whole failure branch unreachable and the + * harness assertion for it vacuous - while the real hazard (an EP0 stall + * during teardown leaving the MAC beaconing) reported success. */ + before = mt_io_errors(dev); + rc = mt_beacon_set_enable(dev, 0, 0); + + /* + * Retract the WHOLE identity, not just the timer. Leaving the APC slot + * programmed means the MAC keeps matching and auto-ACKing for a BSS that + * no longer exists, so a session that stops beaconing and carries on as an + * injector or a monitor drags that residue with it. The bring-up gate + * already zeroes the slot on every exit path for exactly this reason; the + * public path was the weaker of the two. + * + * Slot 1 and slot 0 are both cleared because beacon_start picks between + * them by the address's locally-administered bit, and stop does not have + * the beacon any more to re-derive which one it used. + */ + if (mt_ap_set_bssid(dev, 0, zero)) rc = -1; + if (mt_ap_set_bssid(dev, 1, zero)) rc = -1; + + /* And the port MAC, if this call's opposite number was what retargeted + * it. mt7612u_clear_ack_responder() is the restore. */ + unwind_identity(dev, dev->beacon_took_identity); + if (mt_io_errors(dev) != before) rc = -1; + return rc; +} diff --git a/src/mt7612u/caps.cpp b/src/mt7612u/caps.cpp index e3875601..cdab2157 100644 --- a/src/mt7612u/caps.cpp +++ b/src/mt7612u/caps.cpp @@ -81,6 +81,13 @@ int mt7612u_set_ack_responder(struct mt7612u_dev *d, const uint8_t mac[6]) memcpy(d->ack_saved_mac, d->macaddr, 6); d->ack_saved = 1; } + /* Ownership TRANSFERS to this caller. MT_MAC_ADDR is one register with two + * users - the beacon takes it too - and whoever wrote last owns what is + * there. Without this, a responder armed after StartBeacon would be + * silently disarmed by the matching StopBeacon restoring the factory + * address, and a stop after this call would put back an address the caller + * never asked for. */ + d->beacon_took_identity = 0; dw0 = (uint32_t)mac[0] | ((uint32_t)mac[1] << 8) | ((uint32_t)mac[2] << 16) | ((uint32_t)mac[3] << 24); diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index a99165e0..4fdbf7c3 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -279,6 +279,64 @@ size_t mt7612u_send_packets(struct mt7612u_dev *dev, int mt7612u_set_ack_responder(struct mt7612u_dev *dev, const uint8_t mac[6]); void mt7612u_clear_ack_responder(struct mt7612u_dev *dev); +/* + * Hardware beacon, from the MAC's reserved page. + * + * mt7612u_beacon_start() loads the beacon and arms the TBTT timer; the MAC + * then transmits it on its own at every TBTT, stamping the live 64-bit TSF + * into the timestamp field and assigning the 802.11 sequence number. There is + * no host involvement per beacon and no host jitter. `buf` is one + * radiotap-framed MPDU, the same contract as mt7612u_send_packet(); a bare + * MPDU with no radiotap header is accepted too and airs at OFDM 6 Mbps, the + * rate a beacon wants. + * + * addr2 becomes the MAC's identity - MT_MAC_ADDR (what it ACKs against) and + * the MT_MAC_BSSID base (what the per-BSS index is derived from), moved + * together the way mt76x02_mac_setaddr() moves them. Moving only the first + * leaves the hardware deriving its BSS index from a different address than the + * caller thinks, which is silent: the AP beacons perfectly and matches nobody. + * addr3 is then published in the APC slot that index selects - 1 for a + * locally-administered address, 0 otherwise. + * + * The identity is one register plane, shared with mt7612u_set_ack_responder(): + * a caller doing both is setting the same thing twice and the last writer + * wins. mt7612u_beacon_stop() restores the factory identity ONLY if this call + * was what moved it - if a responder was already armed, that address is the + * caller's and stop leaves it alone. + * + * mt7612u_beacon_update() replaces the loaded beacon in place; the interval, + * TBTT phase and BSSID are untouched. It suppresses the slot for the duration + * of the copy, so a beacon airing across an update carries the PREVIOUS + * content rather than a torn mixture of the two - but it may be skipped + * entirely. + * + * mt7612u_beacon_stop() clears the timer bits, zeroes APC slots 0 and 1, and + * restores the identity as above. It matters that it is called: the MAC + * beacons AUTONOMOUSLY once armed, so killing the host process does NOT + * silence it, and a beacon left airing contaminates whatever runs next on that + * channel. + * + * What mt7612u_beacon_start() REFUSES, all silently fatal if allowed through: + * - a multicast addr2 (a station cannot unicast-auth to it) + * - an 802.11 header that is not 24 bytes. A QoS or 4-address frame makes + * mt_tx_build() insert an interior L2 pad, and the reserved page needs an + * unpadded [TXWI][MPDU] + * - a beacon body that does not fit the 1600-byte slot alongside its TXWI + * - an interval outside 1..4095 TU (INTVAL is 16 bits of 1/16 TU) + * + * And what it FORCES, whatever the caller's radiotap said: no_ack (a broadcast + * beacon must not request an ACK). A bare MPDU with no radiotap header is + * accepted and pinned to OFDM 6 Mbps, NSS 1, 20 MHz - the basic rate every + * station must decode. + * + * All three return 0 on success, negative on failure. A failed stop is a + * beacon still on the air; it is worth retrying. + */ +int mt7612u_beacon_start(struct mt7612u_dev *dev, const void *buf, size_t len, + unsigned interval_tu); +int mt7612u_beacon_update(struct mt7612u_dev *dev, const void *buf, size_t len); +int mt7612u_beacon_stop(struct mt7612u_dev *dev); + /* * TX/RX counters from the async rings. Zeroed when no ring is running, and * taken under the ring's own lock - reading the fields directly would race diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 7fab68e5..d227ab34 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -214,6 +214,11 @@ struct mt7612u_dev { /* Oracle-diff log: every EP0 write we emit, in order. */ uint8_t ack_saved_mac[6]; int ack_saved; + /* Set when mt7612u_beacon_start() was the one that retargeted the port + * identity, so mt7612u_beacon_stop() restores it - and does NOT when a + * caller had already armed an ACK responder, because then the identity is + * theirs and restoring would silently disarm it. */ + int beacon_took_identity; struct mt_async *a; FILE *wrlog; FILE *mculog; @@ -324,6 +329,7 @@ int mt_hdrlen_from_fc(const uint8_t *frame); #define MT_TXOPT_RATE_LUT 0x01 /* set MT_TXWI_FLAGS_TX_RATE_LUT */ #define MT_TXOPT_AMPDU 0x02 /* AMPDU flag + density + BA window */ #define MT_TXOPT_QSEL_MGMT 0x04 /* mt76 uses MT_QSEL_MGMT for aggregated TX */ +#define MT_TXOPT_BEACON 0x08 /* HW timestamp (FLAGS_TS) + HW sequence (ACK_CTL_NSEQ) */ int mt_tx_build(struct mt7612u_dev *d, uint8_t *buf, size_t bufsz, const void *frame, size_t len, const struct mt7612u_tx_rate *rate, uint8_t wcid, unsigned opts, @@ -332,6 +338,19 @@ int mt_tx_raw(struct mt7612u_dev *d, const void *frame, size_t len, const struct mt7612u_tx_rate *rate, uint8_t wcid, unsigned opts); void mt_wcid_setup(struct mt7612u_dev *d, uint8_t idx, const uint8_t *mac); +/* --- beacon.c --- */ +/* Static reserved-page beacon. mt_beacon_init() prepares the beacon engine + * (offsets, bypass, sync) once; mt_beacon_write() loads slot 0; mt_beacon_set_enable() + * arms or disarms auto-TX. No pre-TBTT host work - the MAC beacons on its own. */ +void mt_beacon_init(struct mt7612u_dev *d); +int mt_beacon_write(struct mt7612u_dev *d, const void *frame, size_t len, + const struct mt7612u_tx_rate *rate); +int mt_beacon_set_enable(struct mt7612u_dev *d, int on, unsigned interval_tu); +/* Publish the AP's BSSID in APC slot `idx` so the MAC matches and auto-ACKs + * frames addressed to the BSS. mac_setaddr() zeroes every slot at init. + * Returns 0 on success, -1 if either half of the address failed to program. */ +int mt_ap_set_bssid(struct mt7612u_dev *d, uint8_t idx, const uint8_t *addr); + /* --- radiotap.c --- */ int mt_radiotap_parse(const uint8_t *buf, size_t len, struct mt7612u_tx_rate *r); diff --git a/src/mt7612u/regs.h b/src/mt7612u/regs.h index 80cc7639..9fe9bfd3 100644 --- a/src/mt7612u/regs.h +++ b/src/mt7612u/regs.h @@ -166,19 +166,40 @@ enum mt_mcu_cr_mode { MT_RF_CR, MT_BBP_CR, MT_RF_BBP_CR, MT_HL_TEMP_CR_UPDATE }; #define MT_MAC_ADDR_DW1_U2ME_MASK GENMASK(23, 16) #define MT_MAC_BSSID_DW0 0x1010 #define MT_MAC_BSSID_DW1 0x1014 -#define MT_MAC_BSSID_DW1_MBSS_MODE GENMASK(19, 18) -#define MT_MAC_BSSID_DW1_MBEACON_N GENMASK(22, 20) -#define MT_MAC_BSSID_DW1_MBSS_LOCAL_BIT BIT(23) +/* + * These three were transcribed two bits high (MBSS_MODE at 19:18, MBEACON_N at + * 22:20, LOCAL_BIT at 23) - the whole group shifted left by 2 versus mt76's + * mt76x02_regs.h. It was harmless while this HAL only injected: mac_setaddr() + * is the only user, and MBSS_MODE / MBEACON_N steer beacon generation, which + * an injector never exercises. The AP path beacons, so the true positions + * matter - the shifted masks programmed MBSS_MODE=4 (invalid) and MBEACON_N=15. + */ +#define MT_MAC_BSSID_DW1_ADDR GENMASK(15, 0) +#define MT_MAC_BSSID_DW1_MBSS_MODE GENMASK(17, 16) +#define MT_MAC_BSSID_DW1_MBEACON_N GENMASK(20, 18) +#define MT_MAC_BSSID_DW1_MBSS_LOCAL_BIT BIT(21) +#define MT_MAC_BSSID_DW1_MBSS_MODE_B2 BIT(22) +#define MT_MAC_BSSID_DW1_MBEACON_N_B3 BIT(23) #define MT_MAX_LEN_CFG 0x1018 #define MT_XIFS_TIME_CFG 0x1100 #define MT_XIFS_TIME_CFG_OFDM_SIFS GENMASK(15, 8) #define MT_BKOFF_SLOT_CFG 0x1104 #define MT_BKOFF_SLOT_CFG_CC_DELAY GENMASK(11, 8) #define MT_BEACON_TIME_CFG 0x1114 -#define MT_BEACON_TIME_CFG_INTVAL GENMASK(15, 0) +#define MT_BEACON_TIME_CFG_INTVAL GENMASK(15, 0) /* in 1/16 TU units */ #define MT_BEACON_TIME_CFG_TIMER_EN BIT(16) +#define MT_BEACON_TIME_CFG_SYNC_MODE GENMASK(18, 17) #define MT_BEACON_TIME_CFG_TBTT_EN BIT(19) #define MT_BEACON_TIME_CFG_BEACON_TX BIT(20) +/* Reserved-page beacon (mt76x02_beacon.c). BCN_OFFSET packs four slot offsets + * (each offset/64) per register. BCN_BYPASS_MASK is per-slot and INVERTED: a + * set bit SUPPRESSES that slot (0xffff = all suppressed, used during an + * update); a slot only airs once its bit is CLEARED. mt76 enables N written + * beacons with 0xff00 | ~(0xff00 >> N). */ +#define MT_BCN_OFFSET_BASE 0x041c +#define MT_BCN_OFFSET(_n) (MT_BCN_OFFSET_BASE + ((_n) << 2)) +#define MT_BCN_BYPASS_MASK 0x108c +#define MT_BEACON_BASE 0xc000 #define MT_TSF_TIMER_DW0 0x111c #define MT_TSF_TIMER_DW1 0x1120 #define MT_MAC_STATUS 0x1200 @@ -222,6 +243,8 @@ enum mt_mcu_cr_mode { MT_RF_CR, MT_BBP_CR, MT_RF_BBP_CR, MT_HL_TEMP_CR_UPDATE }; #define MT_RX_FILTR_CFG_PROMISC BIT(2) #define MT_RX_FILTR_CFG_OTHER_BSS BIT(3) #define MT_RX_FILTR_CFG_VER_ERR BIT(4) +#define MT_RX_FILTR_CFG_MCAST BIT(5) +#define MT_RX_FILTR_CFG_BCAST BIT(6) #define MT_RX_FILTR_CFG_DUP BIT(7) #define MT_RX_FILTR_CFG_CTRL_RSV BIT(16) #define MT_AUTO_RSP_CFG 0x1404 @@ -411,6 +434,7 @@ enum mt_ee_field { #define MT_RXWI_LEN 32 #define MT_DMA_HDR_LEN 4 +#define MT_TXWI_FLAGS_TS BIT(3) /* MAC inserts the TSF timestamp (beacon/probe-resp) */ #define MT_TXWI_FLAGS_AMPDU BIT(4) #define MT_TXWI_FLAGS_MPDU_DENSITY GENMASK(7, 5) #define MT_TXWI_ACK_CTL_BA_WINDOW GENMASK(7, 2) diff --git a/src/mt7612u/tests/api_link.c b/src/mt7612u/tests/api_link.c index f6bf5d8e..42cf8572 100644 --- a/src/mt7612u/tests/api_link.c +++ b/src/mt7612u/tests/api_link.c @@ -34,6 +34,9 @@ static void *const api[] = { (void *)mt7612u_send_packets, (void *)mt7612u_set_ack_responder, (void *)mt7612u_clear_ack_responder, + (void *)mt7612u_beacon_start, + (void *)mt7612u_beacon_update, + (void *)mt7612u_beacon_stop, (void *)mt7612u_get_stats, (void *)mt7612u_link_stats_start, (void *)mt7612u_link_stats, diff --git a/src/mt7612u/tests/frame_shape.cpp b/src/mt7612u/tests/frame_shape.cpp index 8fcb72d2..911f0047 100644 --- a/src/mt7612u/tests/frame_shape.cpp +++ b/src/mt7612u/tests/frame_shape.cpp @@ -492,6 +492,56 @@ static void test_tx_ring_ceiling(void) } } +/* + * A reserved-page beacon must carry two TXWI bits an injected frame must not: + * MT_TXWI_FLAGS_TS (the MAC fills the TSF timestamp) and MT_TXWI_ACK_CTL_NSEQ + * (the MAC assigns the sequence number). Without them the witness sees a frozen + * TSF and a constant sequence number - exactly what Gate A checks on air. This + * pins the bits at build time so the on-air gate is not the first to notice. + */ +static void test_beacon_txwi(void) +{ + struct mt7612u_dev d{}; + uint8_t buf[128]; + uint8_t beacon[36], data[36]; + struct mt7612u_tx_rate bcn_rate = { + .phy = MT7612U_PHY_OFDM, .mcs = 0, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1, + }; + struct mt7612u_tx_rate data_rate = { + .phy = MT7612U_PHY_OFDM, .mcs = 0, .nss = 1, .bw = MT7612U_BW_20, + }; + int total; + uint8_t flags_lo, ack_ctl; + + printf("beacon TXWI (MT_TXOPT_BEACON sets TS + NSEQ):\n"); + + d.chainmask = 0x0202; + + memset(beacon, 0, sizeof beacon); + beacon[0] = 0x80; /* FC: mgmt beacon, 24-byte header */ + memset(data, 0, sizeof data); + data[0] = 0x08; /* FC: data, 24-byte header */ + + total = mt_tx_build(&d, buf, sizeof buf, beacon, sizeof beacon, + &bcn_rate, 0xff, MT_TXOPT_BEACON, 0, 0); + if (total < 0) { printf(" FAIL beacon build returned %d\n", total); fails++; return; } + flags_lo = buf[4]; /* TXWI flags, low byte (TXWI is at buf+4) */ + ack_ctl = buf[8]; /* TXWI ack_ctl */ + if (!(flags_lo & MT_TXWI_FLAGS_TS)) { printf(" FAIL beacon missing FLAGS_TS\n"); fails++; } + if (!(ack_ctl & MT_TXWI_ACK_CTL_NSEQ)) { printf(" FAIL beacon missing ACK_CTL_NSEQ\n"); fails++; } + if (ack_ctl & MT_TXWI_ACK_CTL_REQ) { printf(" FAIL beacon requested an ACK\n"); fails++; } + + total = mt_tx_build(&d, buf, sizeof buf, data, sizeof data, + &data_rate, 0xff, 0, 0, 0); + if (total < 0) { printf(" FAIL data build returned %d\n", total); fails++; return; } + flags_lo = buf[4]; + ack_ctl = buf[8]; + if (flags_lo & MT_TXWI_FLAGS_TS) { printf(" FAIL data frame set FLAGS_TS\n"); fails++; } + if (ack_ctl & MT_TXWI_ACK_CTL_NSEQ) { printf(" FAIL data frame set NSEQ\n"); fails++; } + if (!(ack_ctl & MT_TXWI_ACK_CTL_REQ)) { printf(" FAIL data frame did not request an ACK\n"); fails++; } +} + int main(void) { test_hdrlen(); @@ -501,6 +551,7 @@ int main(void) test_chan_group(); test_vht_bandwidth(); test_ht_bandwidth(); + test_beacon_txwi(); printf("frame_shape: %s\n", fails ? "FAIL" : "PASS"); return fails ? 1 : 0; } diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 17cfd941..44ad22a9 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -541,6 +541,376 @@ static int gate_adopt(const char *sel) return rc; } +/* + * Stage A: a static beacon on air. The MAC auto-transmits it from the reserved + * page, so there is nothing to loop over here except watching the TSF advance; + * the RTL8812AU witness (rxdemo) and a kernel station's `iw scan` decide + * PASS/FAIL. The beacon is ALWAYS disabled before returning - a beacon left + * armed keeps airing after the process exits and contaminates the next run. + */ +static int build_beacon(uint8_t chan, const uint8_t *bssid, uint8_t *out, + size_t outsz) +{ + /* 5 GHz: OFDM basic set. 2.4 GHz: CCK + OFDM basic set. */ + static const uint8_t rates_5g[] = { 0x8c, 0x12, 0x98, 0x24, + 0xb0, 0x48, 0x60, 0x6c }; + static const uint8_t rates_2g[] = { 0x82, 0x84, 0x8b, 0x96, + 0x0c, 0x12, 0x18, 0x24 }; + static const char ssid[] = "MT7612U-AP"; + const uint8_t *rates = chan <= 14 ? rates_2g : rates_5g; + const int ssidlen = (int)sizeof ssid - 1; + uint8_t *p = out; + + if (outsz < 128) + return -1; + + *p++ = 0x80; *p++ = 0x00; /* FC: mgmt, beacon */ + *p++ = 0x00; *p++ = 0x00; /* duration */ + memset(p, 0xff, 6); p += 6; /* addr1 = broadcast */ + memcpy(p, bssid, 6); p += 6; /* addr2 = SA (BSSID) */ + memcpy(p, bssid, 6); p += 6; /* addr3 = BSSID */ + *p++ = 0x00; *p++ = 0x00; /* seq ctl (HW assigns) */ + + memset(p, 0, 8); p += 8; /* timestamp (HW fills) */ + *p++ = 0x64; *p++ = 0x00; /* beacon interval = 100 TU */ + *p++ = 0x01; *p++ = 0x00; /* capability: ESS */ + + *p++ = 0; *p++ = (uint8_t)ssidlen; /* SSID IE */ + memcpy(p, ssid, (size_t)ssidlen); p += ssidlen; + *p++ = 1; *p++ = 8; memcpy(p, rates, 8); p += 8; /* Supported Rates */ + *p++ = 3; *p++ = 1; *p++ = chan; /* DS Parameter Set */ + *p++ = 5; *p++ = 4; /* TIM (DTIM=1, empty) */ + *p++ = 0; *p++ = 1; *p++ = 0; *p++ = 0; + + return (int)(p - out); +} + +static int gate_beacon(uint8_t chan, int secs) +{ + /* The AP's BSSID is the device's own MAC, which mac_setaddr() has already + * programmed into MT_MAC_ADDR (what the MAC auto-ACKs against) and + * MT_MAC_BSSID. Advertising anything else in the beacon would leave a + * station addressing auth to an address the MAC does not answer for. It + * is a real, unicast address, which is what a STA requires (an I/G-set + * BSSID makes it drop auth before the air - docs/ap-mode.md). */ + const uint8_t *bssid; + struct mt7612u_tx_rate rate = { + .phy = MT7612U_PHY_OFDM, .mcs = 0, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1, + }; + uint8_t bcn[128]; + int n, rc = 1; + + if (mt_eeprom_init(&dev)) return 1; + bssid = dev.macaddr; + n = build_beacon(chan, bssid, bcn, sizeof bcn); + if (n < 0) { printf("GATE A: FAIL - beacon build\n"); return 1; } + if (mt_init_hardware(&dev, NULL)) { + printf("GATE A: FAIL - init_hardware\n"); return 1; + } + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) { + printf("GATE A: FAIL - set_channel\n"); return 1; + } + /* TX-only: beaconing never reads EP 4. */ + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) { + printf("GATE A: FAIL - mac_start\n"); return 1; + } + + mt_beacon_init(&dev); + if (mt_beacon_write(&dev, bcn, (size_t)n, &rate)) { + printf("GATE A: FAIL - beacon_write\n"); goto out; + } + if (mt_beacon_set_enable(&dev, 1, 100)) { + printf("GATE A: FAIL - beacon_set_enable\n"); goto out; + } + + printf("beacon armed: ch%u, BSSID %02x:%02x:%02x:%02x:%02x:%02x, " + "SSID \"MT7612U-AP\", 100 TU, OFDM 6M, %d B MPDU\n", + chan, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], n); + printf("MT_BEACON_TIME_CFG=0x%08x (bit16 TIMER bit19 TBTT bit20 TX)\n", + mt_rr(&dev, MT_BEACON_TIME_CFG)); + printf("MT_MAC_BSSID_DW1 =0x%08x (MBSS_MODE 17:16 should read 3)\n", + mt_rr(&dev, MT_MAC_BSSID_DW1)); + printf("witness: run rxdemo on the 8812AU and grep the BSSID; " + "or `iw dev scan | grep MT7612U-AP`\n"); + + /* Watch the TSF advance - proof the beacon timer is running. DW0 is the + * low word on this silicon (mt76's debug read has it backwards). */ + { + uint64_t prev = 0; + int good = 0; + + for (int s = 0; s < secs && !g_stop; s++) { + uint32_t lo = mt_rr(&dev, MT_TSF_TIMER_DW0); + uint32_t hi = mt_rr(&dev, MT_TSF_TIMER_DW1); + uint64_t tsf = ((uint64_t)hi << 32) | lo; + + if (s) + printf(" t=%ds TSF=%llu (+%llu us)\n", s, + (unsigned long long)tsf, + (unsigned long long)(tsf - prev)); + if (s && tsf > prev) + good++; + prev = tsf; + if (!wait_ms(1000)) + break; + } + /* A running TSF is necessary, not sufficient - the witness is the + * real gate - but a frozen TSF means no beacons are being sent. */ + if (good == 0) { + printf("GATE A: FAIL - TSF did not advance; beacon timer is dead\n"); + goto out; + } + printf("TSF advanced on %d sample(s) - beacon timer is live\n", good); + } + rc = 0; + /* This is a LOCAL precondition only: an advancing TSF proves the beacon + * timer runs, not that a frame reaches the air. The witness (rxdemo / + * `iw scan`) is the actual Gate A. */ + printf("\nGATE A (local): beacon armed, timer live. On-air PASS/FAIL is " + "the witness's call - grep the 8812AU for our SSID/BSSID.\n"); + +out: + mt_beacon_set_enable(&dev, 0, 0); /* never leave a beacon airing */ + mt_mac_stop(&dev); + return rc; +} + +/* + * Stage B: the beacon plus a receiver, so a real station can probe, authenticate + * and associate against us. + * + * The measurement that matters is the RETRY BIT. An ACK is SIFS-timed and can + * only come from the MAC, so it cannot be observed directly from userspace - + * but a station that does not get one retransmits with FC Retry set. Auth + * arriving at retry=0 is therefore the proof that the hardware auto-ACKed it; + * a pile of retry=1 auths is the proof it did not. + */ +struct ap_ctx { + /* std::atomic, not C11 _Atomic: this file is C++ since the subtree + * migration, and ap_cb runs on the RX event thread while the gate's own + * thread reads the counters. */ + std::atomic probe_req{0}, auth{0}, auth_retry{0}; + std::atomic assoc{0}, assoc_retry{0}; + std::atomic data_to_us{0}, mgmt_other{0}; + uint8_t bssid[6]; +}; + +static void ap_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + struct ap_ctx *c = static_cast(user); + const uint8_t *f = static_cast(frame); + unsigned fc, type, subtype; + int retry, to_us; + + (void)info; + if (len < 16) return; + fc = (unsigned)f[0] | ((unsigned)f[1] << 8); + type = (fc >> 2) & 3; + subtype = (fc >> 4) & 0xf; + retry = (f[1] & 0x08) != 0; /* FC Retry */ + to_us = memcmp(f + 4, c->bssid, 6) == 0; /* addr1 == our BSSID */ + + if (type == 2) { /* data */ + if (to_us) + c->data_to_us.fetch_add(1, std::memory_order_relaxed); + return; + } + if (type != 0) return; /* control */ + + switch (subtype) { + case 4: /* probe request (usually broadcast) */ + c->probe_req.fetch_add(1, std::memory_order_relaxed); + break; + case 11: /* authentication */ + if (!to_us) break; + c->auth.fetch_add(1, std::memory_order_relaxed); + if (retry) + c->auth_retry.fetch_add(1, std::memory_order_relaxed); + break; + case 0: case 2: /* (re)association request */ + if (!to_us) break; + c->assoc.fetch_add(1, std::memory_order_relaxed); + if (retry) + c->assoc_retry.fetch_add(1, std::memory_order_relaxed); + break; + default: + if (to_us) + c->mgmt_other.fetch_add(1, std::memory_order_relaxed); + break; + } +} + +static int gate_ap(uint8_t chan, int secs) +{ + struct ap_ctx ctx{}; + struct mt7612u_tx_rate rate = { + .phy = MT7612U_PHY_OFDM, .mcs = 0, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1, + }; + uint8_t bcn[128]; + int n, rc = 1, rx_up = 0; + unsigned pr, au, aur, as, asr, dt; + + if (secs <= 0 || secs > 3600) { + printf("GATE B: FAIL - duration %d out of range (1..3600 s)\n", secs); + return 1; + } + if (mt_eeprom_init(&dev)) return 1; + memcpy(ctx.bssid, dev.macaddr, 6); + n = build_beacon(chan, dev.macaddr, bcn, sizeof bcn); + if (n < 0) { printf("GATE B: FAIL - beacon build\n"); return 1; } + + if (mt_init_hardware(&dev, NULL)) { + printf("GATE B: FAIL - init_hardware\n"); return 1; + } + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) { + printf("GATE B: FAIL - set_channel\n"); return 1; + } + /* Ring first, receiver second - RX must never run with EP 4 undrained. */ + if (mt7612u_rx_start(&dev, ap_cb, &ctx)) { + printf("GATE B: FAIL - rx_start\n"); return 1; + } + rx_up = 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { + printf("GATE B: FAIL - mac_start\n"); mt7612u_rx_stop(&dev); return 1; + } + /* + * AP receive filter. The managed default mt_mac_start() just wrote already + * leaves OTHER_BSS, BCAST and MCAST undropped, so a probe request with a + * wildcard BSSID reaches us - mt76 clears OTHER_BSS for every mode too. + * The one change an AP needs is DUP: dropping duplicates would hide exactly + * the retransmissions this gate measures. Clear the bit in place rather + * than re-write a copied literal, so this cannot drift from the default. + */ + mt_clear(&dev, MT_RX_FILTR_CFG, MT_RX_FILTR_CFG_DUP); + + /* + * The address-match half of "being an AP": the MAC auto-ACKs against + * MT_MAC_ADDR (already our MAC) and matches the BSS against this slot, + * which mac_setaddr() zeroed. There is no separate AP op-mode register on + * this part - mt76 sets none either; address match + beacon IS the AP. + * + * Slot 0 is only right for a globally-administered MAC. Under MBSS_MODE=3 + * the hardware takes the BSS index from the address bits, and mt76 uses + * 1 + (((macaddr[0] ^ addr[0]) >> 2) & 7) whenever the locally-administered + * bit is set (mt76x02_util.c). Refuse loudly rather than guess: a cloned + * 02:/06:/0a: MAC would match nothing and void every result below. + */ + if (dev.macaddr[0] & 0x02) { + printf("GATE B: FAIL - MAC %02x:.. is locally administered; APC slot 0 " + "is not the slot this MAC selects (mt76 derives 1+n)\n", + dev.macaddr[0]); + goto out; + } + if (mt_ap_set_bssid(&dev, 0, dev.macaddr)) { + printf("GATE B: FAIL - could not program the APC BSSID slot\n"); + goto out; + } + + mt_beacon_init(&dev); + if (mt_beacon_write(&dev, bcn, (size_t)n, &rate)) { + printf("GATE B: FAIL - beacon_write\n"); goto out; + } + if (mt_beacon_set_enable(&dev, 1, 100)) { + printf("GATE B: FAIL - beacon_set_enable\n"); goto out; + } + + printf("AP up: ch%u BSSID/MAC %02x:%02x:%02x:%02x:%02x:%02x SSID \"MT7612U-AP\"\n", + chan, dev.macaddr[0], dev.macaddr[1], dev.macaddr[2], + dev.macaddr[3], dev.macaddr[4], dev.macaddr[5]); + /* Read back BOTH halves of the BSSID: mt_rmw() skips its write when the + * read fails, so printing only the L half would show a correct-looking + * address for a BSSID whose top two bytes never landed. */ + printf("MT_RX_FILTR_CFG=0x%08x APC_BSSID(0)=%04x%08x AUTO_RSP_CFG=0x%08x\n", + mt_rr(&dev, MT_RX_FILTR_CFG), + (unsigned)(mt_rr(&dev, MT_MAC_APC_BSSID_H(0)) & MT_MAC_APC_BSSID_H_ADDR), + mt_rr(&dev, MT_MAC_APC_BSSID_L(0)), + mt_rr(&dev, MT_AUTO_RSP_CFG)); + printf("stimulus: on a station radio run\n" + " sudo iw dev scan (probe requests)\n" + " sudo wpa_supplicant ... / iw dev connect MT7612U-AP\n"); + printf("listening %d s ...\n", secs); + + if (!wait_ticking(secs * 1000.0)) + printf("(interrupted)\n"); + + /* + * Receiver loss belongs next to the verdict: a retried auth we simply + * missed biases the result toward PASS, which is the direction that + * produces a false hardware conclusion. Sample it while the ring still + * EXISTS - mt7612u_rx_stop() tears the ring down and takes its counters + * with it, which reads back as a flat zero and looks like a clean capture. + */ + { + struct mt7612u_stats st; + + mt7612u_get_stats(&dev, &st); + printf("rx frames %llu err %llu invalid %llu dropped %llu\n", + (unsigned long long)st.rx_frames, + (unsigned long long)st.rx_err, + (unsigned long long)st.rx_invalid, + (unsigned long long)st.rx_dropped); + } + + /* + * Now stop the producer, BEFORE reading the verdict counters. ap_cb() runs + * on the RX event thread, and auth/auth_retry are two independent relaxed + * atomics - sampling them live can catch one increment half-applied and + * invert the verdict outright (auth=0 with auth_retry=1 reads as "no auth + * reached us"; auth_retry>auth reads as "every auth was a retry"). + * mt_async_stop() joins the event thread, so after this no callback can + * run. gate_ack orders it the same way. + */ + mt7612u_rx_stop(&dev); + rx_up = 0; + + pr = ctx.probe_req.load(std::memory_order_relaxed); + au = ctx.auth.load(std::memory_order_relaxed); + aur = ctx.auth_retry.load(std::memory_order_relaxed); + as = ctx.assoc.load(std::memory_order_relaxed); + asr = ctx.assoc_retry.load(std::memory_order_relaxed); + dt = ctx.data_to_us.load(std::memory_order_relaxed); + + printf("\nprobe-req %u | auth %u (retry %u) | assoc %u (retry %u) | data-to-us %u | other-mgmt %u\n", + pr, au, aur, as, asr, dt, + ctx.mgmt_other.load(std::memory_order_relaxed)); + + if (!au) { + printf("GATE B: INCONCLUSIVE - no auth reached us " + "(probe-req %u). Did a station try to connect?\n", pr); + } else if (aur == 0) { + printf("GATE B: PASS - %u auth frame(s), none retried: " + "the MAC auto-ACKed them\n", au); + rc = 0; + } else if (aur < au) { + printf("GATE B: PARTIAL - %u auth, %u retried: ACKs land but not always\n", + au, aur); + rc = 0; + } else { + printf("GATE B: FAIL - every auth (%u) was a retry: nothing is ACKing\n", au); + } + +out: + mt_beacon_set_enable(&dev, 0, 0); /* never leave a beacon airing */ + /* Retract the BSS address too, so the teardown matches the contract the + * beacon half states. Inert in practice (the MAC is stopped and + * mac_setaddr() re-zeroes every slot on the next bring-up), but leaving + * half the AP identity programmed contradicts what this gate promises. */ + { + static const uint8_t zero[6] = { 0 }; + + mt_ap_set_bssid(&dev, 0, zero); + } + /* rx_up: the verdict path already stopped the ring so the counters could + * be read with the producer joined; stopping twice must not happen. */ + if (rx_up) + mt7612u_rx_stop(&dev); + mt_mac_stop(&dev); + return rc; +} + /* Gate E: inject frames. The witness is a separate radio - our own RX seeing * these would prove nothing. */ static int gate_tx(uint8_t chan, int count, int phy, int mcs) @@ -2609,6 +2979,12 @@ int main(int argc, char **argv) argc > 3 ? atoi(argv[3]) : 200, argc > 4 ? atoi(argv[4]) : MT7612U_PHY_OFDM, argc > 5 ? atoi(argv[5]) : 0); + } else if (!strcmp(cmd, "beacon")) { + rc = gate_beacon(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 10); + } else if (!strcmp(cmd, "ap")) { + rc = gate_ap(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 30); } else if (!strcmp(cmd, "chan")) { rc = gate_chan(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, argc > 3 ? argv[3] : NULL); @@ -2622,6 +2998,8 @@ int main(int argc, char **argv) fprintf(stderr, "unknown subcommand '%s'\n", cmd); fprintf(stderr, "usage: bringup [regs|fw|init|chan|tx|rx|hop|gateg] [chan] [count] [phy 0=CCK 1=OFDM 2=HT 4=VHT] [mcs]\n"); fprintf(stderr, " bringup adopt (the mt_adopt path a libusb-owning consumer uses)\n"); + fprintf(stderr, " bringup beacon [chan] [secs] (Stage A: static AP beacon on air)\n"); + fprintf(stderr, " bringup ap [chan] [secs] (Stage B: beacon + RX, probe/auth/assoc)\n"); fprintf(stderr, " bringup [sweep|coding|vht] [chan] [count] [bw 0=20 1=40 2=80]\n"); fprintf(stderr, " the witness must listen at the same width (DEVOURER_BW=40|80)\n"); rc = 2; diff --git a/src/mt7612u/tx.cpp b/src/mt7612u/tx.cpp index d3b3a598..3f30c7b8 100644 --- a/src/mt7612u/tx.cpp +++ b/src/mt7612u/tx.cpp @@ -135,6 +135,10 @@ int mt_tx_build(struct mt7612u_dev *d, uint8_t *buf, size_t bufsz, if (opts & MT_TXOPT_AMPDU) fl |= MT_TXWI_FLAGS_AMPDU | FIELD_PREP(MT_TXWI_FLAGS_MPDU_DENSITY, 4); + /* Beacon/probe-resp: the MAC fills the TSF timestamp field, as + * mt76x02_mac_write_txwi() does for these subtypes. */ + if (opts & MT_TXOPT_BEACON) + fl |= MT_TXWI_FLAGS_TS; put_le16(txwi + 0, fl); } /* "A frame may narrow below the channel but never widen it" was only @@ -162,6 +166,10 @@ int mt_tx_build(struct mt7612u_dev *d, uint8_t *buf, size_t bufsz, /* ack_ctl bit0 REQ: set it only when an ACK is wanted. Leaving it * clear is how a frame becomes no-ACK, per packet. */ txwi[4] = rate->no_ack ? 0 : MT_TXWI_ACK_CTL_REQ; + /* Beacon: let the MAC assign the 802.11 sequence number (mt76 sets this + * for IEEE80211_TX_CTL_ASSIGN_SEQ frames), so each beacon airs seq+1. */ + if (opts & MT_TXOPT_BEACON) + txwi[4] |= MT_TXWI_ACK_CTL_NSEQ; if (opts & MT_TXOPT_AMPDU) txwi[4] |= FIELD_PREP(MT_TXWI_ACK_CTL_BA_WINDOW, 63); txwi[5] = wcid; /* 0xff = none */ diff --git a/tests/ap_responder.cpp b/tests/ap_responder.cpp index 2bf1cf31..4f94b760 100644 --- a/tests/ap_responder.cpp +++ b/tests/ap_responder.cpp @@ -290,5 +290,17 @@ int main(int argc, char** argv) { (unsigned long long)g_probe.load(), (unsigned long long)g_auth.load(), (unsigned long long)g_assoc.load(), (unsigned long long)g_data.load(), (unsigned long long)g_sent.load()); + /* Retried, and the failure reported. StopBeacon can now genuinely fail (an + * EP0 stall during teardown), IRadio.h says such a failure "must be retried + * ... before its shared port is reused", and `_exit(0)` below means there is + * no destructor coming to try again. A beacon that survives here survives + * the process. */ + if (g_dev) { + bool silenced = false; + for (int i = 0; i < 3 && !silenced; ++i) silenced = g_dev->StopBeacon(); + if (!silenced) + fprintf(stderr, "WARNING: the beacon could not be stopped - it is still " + "airing; power-cycle the adapter\n"); + } _exit(0); } diff --git a/tests/ap_wpa2.cpp b/tests/ap_wpa2.cpp index 119969aa..5e7f3c87 100644 --- a/tests/ap_wpa2.cpp +++ b/tests/ap_wpa2.cpp @@ -423,5 +423,17 @@ int main(int argc, char** argv) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } fprintf(stderr, "sent=%llu 4way_state=%d\n", (unsigned long long)g_sent.load(), g_state); + /* Retried, and the failure reported. StopBeacon can now genuinely fail (an + * EP0 stall during teardown), IRadio.h says such a failure "must be retried + * ... before its shared port is reused", and `_exit(0)` below means there is + * no destructor coming to try again. A beacon that survives here survives + * the process. */ + if (g_dev) { + bool silenced = false; + for (int i = 0; i < 3 && !silenced; ++i) silenced = g_dev->StopBeacon(); + if (!silenced) + fprintf(stderr, "WARNING: the beacon could not be stopped - it is still " + "airing; power-cycle the adapter\n"); + } _exit(0); } diff --git a/tests/mt7612u_ap_onair.sh b/tests/mt7612u_ap_onair.sh new file mode 100755 index 00000000..5b05d251 --- /dev/null +++ b/tests/mt7612u_ap_onair.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env bash +# mt7612u_ap_onair.sh — the whole MT7612U AP claim, end to end, on hardware. +# +# Every number in docs/mt7612u-ap-mode.md's "Verified through IRadio" table +# comes from this script. It exists because those numbers were hand-run first, +# and three of the readings were wrong in ways that a script would not have +# repeated: +# +# - "the beacon was gone after the process exited" measured nothing. Both AP +# harnesses used to end in _exit(0), so StopBeacon never ran; the beacon +# was still airing and one scan happened to miss it. +# - `iw scan` without `flush` reports a stopped beacon as present for ~30 s +# out of its BSS cache. +# - a bring-up that fails leaves no beacon either, so "no beacon" read as a +# pass when the AP had in fact never started. Every phase here checks the +# AP came up BEFORE it believes an absence. +# +# Three cells, each with its own witness: +# +# open ap_responder beacon -> scan, associate, ARP/ICMP ping +# wpa2 ap_wpa2 beacon -> scan, 4-way handshake, encrypted ping +# stop beacon_stop_check armed -> stopped -> re-armed, by scan +# +# Bench: two MT7612U. One is the AP (devourer claims it); the other stays on +# the kernel mt76x2u driver and is the station. They are told apart by sysfs +# id, not by PID - they share one. +# +# sudo tests/mt7612u_ap_onair.sh +# sudo AP_SYSFS=5-1 STA_SYSFS=2-1 CH=36 tests/mt7612u_ap_onair.sh open +# +# Env: AP_SYSFS, STA_SYSFS, CH, PSK, FW_DIR, SECS. Cells: open|wpa2|stop|all. + +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${BUILD:-$ROOT/build}" +CELLS="${1:-all}" + +AP_SYSFS="${AP_SYSFS:-5-1}" +STA_SYSFS="${STA_SYSFS:-2-1}" +CH="${CH:-36}" +FREQ=$(( CH < 15 ? 2407 + CH * 5 : 5000 + CH * 5 )) +PSK="${PSK:-devourer123}" +SECS="${SECS:-40}" +FW_DIR="${FW_DIR:-}" +APIP=192.168.99.1 +STAIP=192.168.99.2 +OUT="${OUT:-/tmp/mt7612u-ap-onair}" + +[ "$(id -u)" = 0 ] || { echo "must run as root"; exit 2; } +mkdir -p "$OUT" +pass=0; fail=0 +say() { printf '%s\n' "$*"; } +ok() { pass=$((pass+1)); printf ' PASS %s\n' "$*"; } +bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$*"; } + +# PIDs this script started, so cleanup kills those and nothing else. `pkill -x +# wpa_supplicant` would drop every wireless client on the host, and a name kill +# would reach a concurrent run of this same test. +KIDS="" +reap() { + local pid + for pid in $KIDS; do kill "$pid" 2>/dev/null; done + KIDS="" +} + +cleanup() { + reap + [ -n "${STA_IF:-}" ] && { ip addr flush dev "$STA_IF" 2>/dev/null + iw dev "$STA_IF" disconnect 2>/dev/null; } + # The MAC beacons autonomously. If a cell died before its teardown, only a + # port power-cycle is certain to silence it - and leaving one airing poisons + # the next run of this very script. + # + # Confirmed against the VID:PID first. This runs as root and writes a + # deauthorize to a path the caller supplied; a stale or mistyped AP_SYSFS + # would otherwise yank whatever else is plugged there - someone's keyboard, + # a disk mid-write. + if [ "$(cat "/sys/bus/usb/devices/$AP_SYSFS/idVendor" 2>/dev/null)" = "0e8d" ] && + [ "$(cat "/sys/bus/usb/devices/$AP_SYSFS/idProduct" 2>/dev/null)" = "7612" ]; then + echo 0 > "/sys/bus/usb/devices/$AP_SYSFS/authorized" 2>/dev/null + sleep 2 + echo 1 > "/sys/bus/usb/devices/$AP_SYSFS/authorized" 2>/dev/null + sleep 3 + fi +} +trap cleanup EXIT INT TERM + +# --- the station ----------------------------------------------------------- +STA_IF=$(ls "/sys/bus/usb/devices/$STA_SYSFS:1.0/net/" 2>/dev/null | head -1) +if [ -z "$STA_IF" ]; then + echo "$STA_SYSFS:1.0" > /sys/bus/usb/drivers_probe 2>/dev/null + sleep 3 + STA_IF=$(ls "/sys/bus/usb/devices/$STA_SYSFS:1.0/net/" 2>/dev/null | head -1) +fi +[ -n "$STA_IF" ] || { echo "no station iface at $STA_SYSFS (is mt76x2u bound?)"; exit 2; } +ip link set "$STA_IF" up 2>/dev/null +say "AP $AP_SYSFS station $STA_SYSFS ($STA_IF) ch$CH ($FREQ MHz)" + +# `flush` is not optional: without it the BSS cache reports a beacon that +# stopped up to ~30 s ago as still present, which is how a broken StopBeacon +# reads as working. +# Scan up to three times and take the HIGHEST count. A scan can come back empty +# for its own reasons - colliding with another scan, a busy card, a dwell that +# misses a 100 TU beacon - and one empty result is not evidence of absence. +# Taking the max is the conservative reading in BOTH directions: it cannot turn +# a live beacon into a pass for "gone", and it stops a missed scan reporting a +# live beacon as absent. Observed: a "beacon not scannable" FAIL in a run where +# the station then associated, pinged, and got an auth at retry=0. +seen() { # $1 = SSID, $2 = BSSID + local i n best=0 + for i in 1 2 3; do + # Matched on BSSID *and* SSID: a neighbour running "devourerAP" would + # otherwise pass an arm check, fail a stop check, or break the exact-count + # comparison. awk keeps the pairing - grep -c on two patterns would count + # them independently. + n=$(iw dev "$STA_IF" scan flush freq "$FREQ" 2>/dev/null | + awk -v b="$2" -v ss="SSID: $1" ' + /^BSS / { cur = tolower($2); sub(/\(.*/, "", cur) } + index($0, ss) { if (cur == tolower(b)) c++ } + END { print c + 0 }') + n=${n:-0} + [ "$n" -gt "$best" ] && best=$n + [ "$best" -gt 0 ] && break + sleep 2 + done + printf '%s' "$best" +} + +apenv() { + set -- DEVOURER_VID=0x0e8d DEVOURER_PID=0x7612 DEVOURER_CHANNEL="$CH" \ + DEVOURER_BCN_TU=100 DEVOURER_TX_WITH_RX=thread "$@" + [ -n "$FW_DIR" ] && set -- DEVOURER_MT7612U_FW_DIR="$FW_DIR" "$@" + printf '%s\n' "$@" +} + +build() { # $1 = source stem, $2 = output name, $3.. = extra libs + local src="$1" out="$2"; shift 2 + g++ -std=c++20 -O2 -I"$ROOT/src" -I"$ROOT/examples/common" \ + "$ROOT/tests/$src.cpp" "$ROOT/examples/common/env_config.cpp" \ + "$BUILD/libdevourer.a" $(pkg-config --cflags --libs libusb-1.0) \ + "$@" -lpthread -o "/tmp/$out" || return 1 +} + +# A cell must prove the AP CAME UP before it may believe any absence. A failed +# bring-up beacons nothing, which otherwise reads as a pass. +came_up() { grep -q "beaconing every" "$1"; } + +# --- cell: open network ---------------------------------------------------- +cell_open() { + say "== open network (ap_responder) ==" + build ap_responder apr_onair || { bad "open: build"; return; } + env $(apenv) timeout $((SECS + 20)) /tmp/apr_onair "$SECS" \ + >"$OUT/open.jsonl" 2>"$OUT/open.log" & + local ap=$!; KIDS="$KIDS $ap" + sleep 12 + came_up "$OUT/open.log" || { bad "open: AP did not come up (see $OUT/open.log)"; kill $ap 2>/dev/null; return; } + ok "open: beacon armed" + + [ "$(seen devourerAP 02:42:75:05:d6:00)" = 1 ] && ok "open: beacon on air" || bad "open: beacon not scannable" + + ip addr flush dev "$STA_IF" 2>/dev/null + if timeout 30 iw dev "$STA_IF" connect -w devourerAP >/dev/null 2>&1; then + ok "open: station associated" + else + bad "open: station did not associate"; kill $ap 2>/dev/null; return + fi + + ip addr add "$STAIP/24" dev "$STA_IF" 2>/dev/null + ping -c 1 -W 2 -I "$STA_IF" "$APIP" >/dev/null 2>&1 # warm ARP + if ping -c 6 -W 1 -I "$STA_IF" "$APIP" 2>&1 | tee "$OUT/open.ping" | grep -q " 0% packet loss"; then + ok "open: data plane ($(grep -oE 'rtt [^ ]+ = [0-9./]+' "$OUT/open.ping" | head -1))" + else + bad "open: ping lost packets ($(grep -oE '[0-9]+% packet loss' "$OUT/open.ping" | head -1))" + fi + # retry=0 on auth IS the hardware ACK: an un-ACKed frame comes back with FC + # Retry set. This is the only evidence that the APC slot and port identity + # are both right. + grep -q "AUTH req .* retry=0" "$OUT/open.log" \ + && ok "open: hardware auto-ACK (auth at retry=0)" \ + || bad "open: no auth at retry=0 - the MAC did not ACK" + + iw dev "$STA_IF" disconnect 2>/dev/null; ip addr flush dev "$STA_IF" 2>/dev/null + wait $ap 2>/dev/null + sleep 3 + [ "$(seen devourerAP 02:42:75:05:d6:00)" = 0 ] \ + && ok "open: nothing left airing after exit" \ + || bad "open: beacon STILL AIRING after exit" +} + +# --- cell: WPA2-PSK -------------------------------------------------------- +cell_wpa2() { + say "== WPA2-PSK (ap_wpa2) ==" + build ap_wpa2 apw_onair -lcrypto || { bad "wpa2: build"; return; } + env $(apenv) DEVOURER_WPA2_PSK="$PSK" timeout $((SECS + 20)) /tmp/apw_onair "$SECS" \ + >"$OUT/wpa2.jsonl" 2>"$OUT/wpa2.log" & + local ap=$!; KIDS="$KIDS $ap" + sleep 12 + came_up "$OUT/wpa2.log" || { bad "wpa2: AP did not come up (see $OUT/wpa2.log)"; kill $ap 2>/dev/null; return; } + ok "wpa2: beacon armed" + + local wpa="$OUT/wpa.conf" + printf 'network={\n\tssid="devourerAP"\n\tpsk="%s"\n\tkey_mgmt=WPA-PSK\n\tproto=RSN\n\tpairwise=CCMP\n\tgroup=CCMP\n\tscan_ssid=1\n}\n' "$PSK" > "$wpa" + ip addr flush dev "$STA_IF" 2>/dev/null + wpa_supplicant -i "$STA_IF" -c "$wpa" -P "$OUT/wpa.pid" -B >/dev/null 2>&1 + KIDS="$KIDS $(cat "$OUT/wpa.pid" 2>/dev/null)" + local i + for i in $(seq 1 20); do + grep -q "4-WAY HANDSHAKE COMPLETE" "$OUT/wpa2.log" && break + sleep 1 + done + if grep -q "4-WAY HANDSHAKE COMPLETE" "$OUT/wpa2.log"; then + ok "wpa2: 4-way complete (MIC verified, station keyed)" + else + bad "wpa2: 4-way did not complete" + kill "$(cat "$OUT/wpa.pid" 2>/dev/null)" 2>/dev/null + kill $ap 2>/dev/null; return + fi + + ip addr add "$STAIP/24" dev "$STA_IF" 2>/dev/null + ping -c 1 -W 2 -I "$STA_IF" "$APIP" >/dev/null 2>&1 + if ping -c 6 -W 1 -I "$STA_IF" "$APIP" 2>&1 | tee "$OUT/wpa2.ping" | grep -q " 0% packet loss"; then + ok "wpa2: encrypted data plane ($(grep -oE 'rtt [^ ]+ = [0-9./]+' "$OUT/wpa2.ping" | head -1))" + else + bad "wpa2: encrypted ping lost packets" + fi + + kill "$(cat "$OUT/wpa.pid" 2>/dev/null)" 2>/dev/null + ip addr flush dev "$STA_IF" 2>/dev/null + wait $ap 2>/dev/null + sleep 3 + [ "$(seen devourerAP 02:42:75:05:d6:00)" = 0 ] \ + && ok "wpa2: nothing left airing after exit" \ + || bad "wpa2: beacon STILL AIRING after exit" +} + +# --- cell: the beacon lifecycle ------------------------------------------- +cell_stop() { + say "== beacon lifecycle (StartBeacon / StopBeacon / re-arm) ==" + build mt7612u_beacon_stop_check bstop_onair || { bad "stop: build"; return; } + local phase=24 + env $(apenv) timeout $((phase * 3 + 40)) /tmp/bstop_onair "$phase" \ + >"$OUT/stop.log" 2>&1 & + local ap=$!; KIDS="$KIDS $ap" + + # Wait for the ARM ITSELF, not for the phase banner. The banner prints + # before StartBeacon, and the arm is not instant - it copies a 1600-byte + # page over EP0 and reads the identity back. Sleeping a guessed interval + # after the banner is how phase 1 of this very cell reported "armed but not + # scannable" while phase 3, which happened to sleep longer, passed. + # grep -c PRINTS 0 and EXITS 1 when it matches nothing, so `|| echo 0` + # appends a second line and every later [ -gt ] dies on "0\n0". + armed() { + local n + n=$(grep -c "beaconing every" "$OUT/stop.log" 2>/dev/null) + printf '%s' "${n:-0}" + } + wait_arm() { # $1 = the count to exceed, $2 = seconds to wait + local i + for i in $(seq 1 "$2"); do [ "$(armed)" -gt "$1" ] && return 0; sleep 1; done + return 1 + } + wait_gone() { # the log line that says StopBeacon ran, then a settle + local i + for i in $(seq 1 "$1"); do grep -q "PHASE 3" "$OUT/stop.log" && return 0; sleep 1; done + return 1 + } + + wait_arm 0 30 || { bad "stop: never armed"; kill $ap 2>/dev/null; return; } + sleep 4 + [ "$(seen mtStopCheck 02:4d:54:53:54:50)" = 1 ] && ok "stop: armed - beacon on air" || bad "stop: armed but not scannable" + + local n_arms; n_arms=$(armed) + local i + for i in $(seq 1 60); do grep -q "PHASE 2" "$OUT/stop.log" && break; sleep 1; done + sleep 6 + [ "$(seen mtStopCheck 02:4d:54:53:54:50)" = 0 ] && ok "stop: stopped - beacon gone" || bad "stop: STILL AIRING after StopBeacon" + + # The re-arm is the same non-instant operation: wait for the second + # "beaconing every", not for the banner that precedes it. + wait_arm "$n_arms" 60 || { bad "stop: re-arm never reported"; kill $ap 2>/dev/null; return; } + sleep 4 + [ "$(seen mtStopCheck 02:4d:54:53:54:50)" = 1 ] && ok "stop: re-armed - beacon back" || bad "stop: re-arm did not air" + + wait $ap 2>/dev/null + grep -q "0 failure(s)" "$OUT/stop.log" \ + && ok "stop: local contract checks (2nd stop false, update-with-no-beacon false)" \ + || bad "stop: local contract checks failed" +} + +case "$CELLS" in + open) cell_open ;; + wpa2) cell_wpa2 ;; + stop) cell_stop ;; + all) cell_open; cleanup; cell_wpa2; cleanup; cell_stop ;; + *) echo "usage: $0 [open|wpa2|stop|all]"; exit 2 ;; +esac + +say "" +say "=== $pass passed, $fail failed (logs: $OUT) ===" +exit $(( fail > 0 )) diff --git a/tests/mt7612u_beacon_stop_check.cpp b/tests/mt7612u_beacon_stop_check.cpp new file mode 100644 index 00000000..5f463b26 --- /dev/null +++ b/tests/mt7612u_beacon_stop_check.cpp @@ -0,0 +1,172 @@ +/* + * Does StopBeacon actually silence the MAC? + * + * This exists because the obvious way to check it did not check it. Both AP + * harnesses used to end in `_exit(0)` with no teardown, which bypasses every + * destructor - so the radio's Stop(), and with it StopBeacon(), never ran, and + * "the beacon was gone after the process exited" measured nothing. They call + * StopBeacon explicitly now, but that still tests the call from one place at + * one moment; this drives the whole transition and lets a witness look. On this part that is not a cosmetic difference: the MAC + * beacons AUTONOMOUSLY from the reserved page once armed, so a session that + * skips the teardown leaves it airing until the adapter is power-cycled. + * + * Three phases, each long enough for a station to complete a scan: + * 1. armed - a scan MUST see the SSID + * 2. stopped - StopBeacon(), then a scan MUST NOT + * 3. re-armed - StartBeacon() again, to prove the stop left the engine + * usable rather than wedged + * + * The witness is external (`iw scan` from a station); this program only drives + * the transitions and prints when each phase begins, so the operator or a + * script can scan in the right window. It is not a ctest cell - it needs an + * adapter and a second radio to look. + * + * Build: + * g++ -std=c++20 -O2 -Isrc -Iexamples/common \ + * tests/mt7612u_beacon_stop_check.cpp examples/common/env_config.cpp \ + * build/libdevourer.a $(pkg-config --cflags --libs libusb-1.0) \ + * -lpthread -o beacon_stop_check + * + * sudo DEVOURER_VID=0x0e8d DEVOURER_PID=0x7612 DEVOURER_CHANNEL=36 \ + * DEVOURER_MT7612U_FW_DIR= ./beacon_stop_check [phase_secs] + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "DeviceConfig.h" +#include "UsbDeviceLock.h" +#include "UsbOpen.h" +#include "WiFiDriver.h" +#include "env_config.h" +#include "logger.h" + +namespace { + +/* BSSID and SSID of our own, so this cannot be confused with a neighbour or + * with ap_responder's devourerAP. Locally administered on purpose: that is the + * case that lands in APC slot 1 rather than 0, and the case the first draft of + * mt7612u_beacon_start() refused outright. */ +const uint8_t kBssid[6] = { 0x02, 0x4d, 0x54, 0x53, 0x54, 0x50 }; +const char kSsid[] = "mtStopCheck"; + +std::vector build_beacon(uint8_t chan) { + std::vector f = { + /* radiotap: 8-byte header + TX flags, the shape ap_responder uses */ + 0x00, 0x00, 0x0a, 0x00, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, + 0x80, 0x00, 0x00, 0x00, /* FC + duration */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* addr1 broadcast */ + }; + f.insert(f.end(), kBssid, kBssid + 6); /* addr2 */ + f.insert(f.end(), kBssid, kBssid + 6); /* addr3 */ + const uint8_t tail[] = { + 0x00, 0x00, /* seq (HW assigns) */ + 0, 0, 0, 0, 0, 0, 0, 0, /* timestamp (HW) */ + 0x64, 0x00, /* 100 TU */ + 0x01, 0x00, /* ESS */ + 0x00, (uint8_t)(sizeof kSsid - 1), + }; + f.insert(f.end(), tail, tail + sizeof tail); + f.insert(f.end(), kSsid, kSsid + sizeof kSsid - 1); + const uint8_t ies[] = { + 0x01, 0x08, 0x8c, 0x12, 0x98, 0x24, 0xb0, 0x48, 0x60, 0x6c, + 0x03, 0x01, chan, + }; + f.insert(f.end(), ies, ies + sizeof ies); + return f; +} + +void banner(const char *phase) { + std::printf("\n=== %s === (scan now)\n", phase); + std::fflush(stdout); +} + +} // namespace + +int main(int argc, char **argv) { + const int secs = argc > 1 ? atoi(argv[1]) : 20; + uint8_t chan = 36; + if (const char *c = std::getenv("DEVOURER_CHANNEL")) chan = (uint8_t)atoi(c); + + auto logger = std::make_shared(); + libusb_context *ctx = nullptr; + libusb_init(&ctx); + libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_WARNING); + + uint16_t vid = 0x0e8d, pid = 0x7612; + if (const char *v = std::getenv("DEVOURER_VID")) vid = (uint16_t)strtoul(v, 0, 0); + if (const char *p = std::getenv("DEVOURER_PID")) pid = (uint16_t)strtoul(p, 0, 0); + auto *h = libusb_open_device_with_vid_pid(ctx, vid, pid); + if (!h) { std::fprintf(stderr, "open %04x:%04x fail\n", vid, pid); return 1; } + + std::shared_ptr lk; + if (devourer::claim_interface_then_reset( + h, devourer::find_wifi_interface(h), logger, true, lk) != 0) + return 1; + + WiFiDriver wifi(logger); + auto dev = wifi.CreateRadio(h, ctx, lk, devourer_config_from_env()); + if (!dev) return 1; + dev->InitWrite(SelectedChannel{chan, 0, CHANNEL_WIDTH_20}); + + const auto bcn = build_beacon(chan); + int fails = 0; + + banner("PHASE 1: armed - the SSID mtStopCheck MUST appear"); + if (!dev->StartBeacon(bcn.data(), bcn.size(), 100)) { + std::fprintf(stderr, "FAIL: StartBeacon returned false\n"); + return 1; + } + std::this_thread::sleep_for(std::chrono::seconds(secs)); + + banner("PHASE 2: stopped - the SSID MUST be gone"); + if (!dev->StopBeacon()) { + std::fprintf(stderr, "FAIL: StopBeacon returned false\n"); + fails++; + } + /* Documented to return false the second time: no beacon is active. */ + if (dev->StopBeacon()) { + std::fprintf(stderr, "FAIL: a second StopBeacon returned true\n"); + fails++; + } + /* And an update with nothing armed must refuse rather than report success + * for a write into a disarmed engine. */ + if (dev->UpdateBeaconPayload(bcn.data(), bcn.size())) { + std::fprintf(stderr, "FAIL: UpdateBeaconPayload succeeded with no beacon\n"); + fails++; + } + std::this_thread::sleep_for(std::chrono::seconds(secs)); + + banner("PHASE 3: re-armed - the SSID MUST come back"); + if (!dev->StartBeacon(bcn.data(), bcn.size(), 100)) { + std::fprintf(stderr, "FAIL: StartBeacon after a stop returned false\n"); + fails++; + } + std::this_thread::sleep_for(std::chrono::seconds(secs)); + + /* Explicit, not left to the destructor - the point of this program is that + * the teardown path is the thing under test. */ + { + /* Retried like the AP harnesses: the point of this program is the teardown + * path, so leaving its own beacon airing would be the worst possible way + * to end it. */ + bool silenced = false; + for (int i = 0; i < 3 && !silenced; ++i) silenced = dev->StopBeacon(); + if (!silenced) { + std::fprintf(stderr, "FAIL: the final StopBeacon never succeeded - the " + "beacon is still airing\n"); + fails++; + } + } + std::printf("\nlocal checks: %d failure(s). The SSID phases are the " + "witness's call.\n", fails); + return fails ? 1 : 0; +} diff --git a/tests/ul_trigger_ap.cpp b/tests/ul_trigger_ap.cpp index c61d514f..671ed475 100644 --- a/tests/ul_trigger_ap.cpp +++ b/tests/ul_trigger_ap.cpp @@ -446,5 +446,17 @@ int main(int argc, char** argv) { (unsigned long long)g_ul.load(), (unsigned long long)g_ul_tb.load(), (unsigned long long)g_data.load(), (unsigned long long)g_sent.load()); + /* Retried, and the failure reported. StopBeacon can now genuinely fail (an + * EP0 stall during teardown), IRadio.h says such a failure "must be retried + * ... before its shared port is reused", and `_exit(0)` below means there is + * no destructor coming to try again. A beacon that survives here survives + * the process. */ + if (g_dev) { + bool silenced = false; + for (int i = 0; i < 3 && !silenced; ++i) silenced = g_dev->StopBeacon(); + if (!silenced) + fprintf(stderr, "WARNING: the beacon could not be stopped - it is still " + "airing; power-cycle the adapter\n"); + } _exit(0); }