Lab rXg (fwd), 2026‑10‑14. Every-BSSID sweep, two-sensor client-to-client matrix, a ghost-station persistence test, and 4.2.4 on QSDK APs. Follows the Oct 5 sweep, which ran on the 1.18 alpha.
| Component | Version |
|---|---|
| rXg | 16.827-9-hbhowksdan51update · branch hbh/ow-ksdan-5-1-update @ 9f7f7567b3 |
| rxg_sdan bundle served by the rXg | 1.20-ac064a8 (module and rxg_sdanctl report 1.20, commit ac064a82) |
| AP firmware, 5.1 APs | ApNos-92cbcec63 · wlan-ap rgnets-5.1.0-0.006 · OpenWrt 25.12.3 r32912-6639b15f62 |
| AP firmware, 4.2.4 APs | TIP-rgnets-4.2.4-0.011-954918b4e · OpenWrt 23.05 r23485+104-e92cf0c46f (the EAP111 throughout, via its per-AP override; the WF660a and WF186h in section 6) |
| rg_wss_client (all APs) | 1.0.41 |
| AP | Model | Firmware | Build key | How 1.20 arrived | Module SHA-256 |
|---|---|---|---|---|---|
| 189 | CIG WF189 | 5.1.0-0.006 | ipq53xx/generic 6.6.88 25.12 | fresh flash | 58e96a22c08c |
| 660a | Cigtech WF-660a | 5.1.0-0.006 | ipq60xx/generic 5.4.164 25.12 | hot swap, no reboot (from 4779cd2071db) | dcbf09ef9406 |
| 186h | CIG WF186h | 5.1.0-0.006 | ipq50xx/generic 5.4.164 25.12 | fresh flash | a5e8d323dc06 |
| 188n | Cigtech WF-188n | 5.1.0-0.006 | ipq60xx/generic 5.4.164 25.12 | hot swap, no reboot (from 4779cd2071db) | dcbf09ef9406 |
| eap111 | Edgecore EAP111 | 4.2.4-0.011 | mediatek/filogic 6.6.89 23.05 | hot swap, no reboot (from 510d83977bfc) | cb9442f5a94f |
Every hash matches the file the rXg serves for that build. The hot swaps used the delete_module syscall that 1.0.41 introduced. The 186h's and 189's builds were checked after the profile reflash to 5.1.0-0.006.
| AP | 5 GHz | 2.4 GHz | 6 GHz |
|---|---|---|---|
| 189 | pass · ch 44 · 15/15 | pass · ch 6 · 15/15 | pass · ch 65 · 15/15 |
| 660a | pass · ch 149 · 15/15 | pass · ch 6 · 15/15 | no 6 GHz radio |
| 186h | pass · ch 157 · 15/15 | pass · ch 11 · 15/15 | no 6 GHz radio |
| 188n | pass · ch 157 · 15/15 | pass · ch 6 · 15/15 | no 6 GHz radio |
| eap111 | pass · ch 149 · 15/15 | pass · ch 11 · 15/15 | no 6 GHz radio |
| AP | Band | MHz | BSSID | SSID / auth | Lease | Ping |
|---|---|---|---|---|---|---|
| 189 | 5 | 5220 | d4:ba:ba:a3:15:ab | OW mPSK WPA2 · WPA2-PSK | 10.20.2.3 | 15/15 |
| 660a | 5 | 5745 | d4:ba:ba:a1:8c:01 | OW mPSK WPA2 · WPA2-PSK | 10.20.2.3 | 15/15 |
| 186h | 5 | 5785 | d4:ba:ba:a4:05:cb | OW mPSK WPA2 · WPA2-PSK | 10.20.2.3 | 15/15 |
| 188n | 5 | 5785 | d4:ba:ba:ab:ba:aa | OW mPSK WPA2 · WPA2-PSK | 10.20.2.3 | 15/15 |
| eap111 | 5 | 5745 | 22:1a:e3:db:53:68 | OW mPSK WPA2 · WPA2-PSK | 10.20.2.3 | 15/15 |
| 189 | 2.4 | 2437 | d4:ba:ba:a3:15:aa | OW mPSK WPA2 · WPA2-PSK | 10.20.2.4 | 15/15 |
| 660a | 2.4 | 2437 | d4:ba:ba:a1:8c:02 | OW mPSK WPA2 · WPA2-PSK | 10.20.2.4 | 15/15 |
| 186h | 2.4 | 2462 | d4:ba:ba:a4:05:ca | OW mPSK WPA2 · WPA2-PSK | 10.20.2.4 | 15/15 |
| 188n | 2.4 | 2437 | d4:ba:ba:ab:ba:ab | OW mPSK WPA2 · WPA2-PSK | 10.20.2.4 | 15/15 |
| eap111 | 2.4 | 2462 | a0:1a:e3:db:53:68 | OW mPSK WPA2 · WPA2-PSK | 10.20.2.4 | 15/15 |
| 189 | 6 | 6275 | d6:ba:ba:a3:15:ac | OW mPSK WPA3 6GHz · WPA3-SAE (H2E, PMF) | 10.20.2.5 | 15/15 |
Client: s1, a WF189 on 5.1.0-0.006 acting as a sensor. Every BSSID associated and leased on the first attempt, all on VLAN 301 (10.20.2.0/24). The 6 GHz client presents the MAC registered on the mPSK account, because WPA3-SAE binds the key to the station MAC.
Two sensors: A = s1 (WF189, 5.1) and B = the EAP111 (4.2.4, MediaTek). Each case places both, leases both, then in each direction sends 10 ICMP echoes, 1 MB of TCP each way, and 200 × 1200-byte UDP datagrams counted by the receiver. rxg_sdanctl counters on the AP(s) are read before and after, so a pass shows which path carried it.
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx |
|---|---|---|---|---|---|---|
| same-2.4 | 189/2.4 | 189/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| same-2.4 | 660a/2.4 | 660a/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| same-2.4 | 186h/2.4 | 186h/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| same-2.4 | 188n/2.4 | 188n/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| cross-band | 189/5 | 189/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 4419 | 0 |
| cross-band | 660a/5 | 660a/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 4463 | 0 |
| cross-band | 186h/5 | 186h/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 4144 | 0 |
| cross-band | 188n/5 | 188n/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 4393 | 0 |
| same-5 | 189/5 | 189/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| same-5 | 660a/5 | 660a/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| same-5 | 186h/5 | 186h/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| same-5 | 188n/5 | 188n/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| cross-ap | 189/5 | 660a/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| cross-ap | 660a/5 | 186h/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| cross-ap | 186h/5 | 188n/5 | icmp 0/10 · tcp FAIL · udp 0/200 | icmp 0/10 · tcp FAIL · udp 0/200 | 0 | 0 |
| cross-ap | 188n/5 | 189/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| cross-band-6 | 189/6 | 189/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 3661 | 0 |
186h ↔ 188n failed in both directions, in both orders, and with the sensors swapped; every other AP pair passed. The 186h and 188n are the only two APs whose 5 GHz radios share a channel (157). Taking one sensor off that channel mostly cleared it: 2.4 ↔ 2.4 and 186h/2.4 ↔ 188n/5 passed, while 186h/5 ↔ 188n/2.4 still failed one way.
186h ↔ 188n, both orders
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx |
|---|---|---|---|---|---|---|
| cross-ap | 186h/5 | 188n/5 | icmp 0/10 · tcp FAIL · udp 0/200 | icmp 0/10 · tcp FAIL · udp 0/200 | 0 | 0 |
| cross-ap | 188n/5 | 186h/5 | icmp 0/10 · tcp FAIL · udp 0/200 | icmp 0/10 · tcp FAIL · udp 0/200 | 0 | 0 |
186h ↔ 189, both orders
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx |
|---|---|---|---|---|---|---|
| cross-ap | 189/5 | 186h/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| cross-ap | 186h/5 | 189/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
188n ↔ 660a, both orders
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx |
|---|---|---|---|---|---|---|
| cross-ap | 660a/5 | 188n/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 196/200 | 0 | 0 |
| cross-ap | 188n/5 | 660a/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
186h ↔ 188n, other band combinations
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx |
|---|---|---|---|---|---|---|
| pair | 186h/2.4 | 188n/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| pair | 186h/5 | 188n/2.4 | icmp 0/10 · tcp FAIL · udp 400/200 | icmp 10/10 · tcp ok · udp 200/200 | 13 | 0 |
| pair | 186h/2.4 | 188n/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
186h same-5 alone, after the re-run
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx |
|---|---|---|---|---|---|---|
| pair | 186h/5 | 186h/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
After a failure the 188n's hostapd still listed both sensors as associated and authorized on VLAN 301 while they were on the 186h, and its sdan had learned their MACs as local. Joining on a shared channel often lands on the other AP first and then ROAMs to the target, which sends no deauth, so each join can leave a ghost behind. A re-run of the full matrix with a clean deauth after every case still scored 14/17, with every failure on the 186h's 5 GHz BSS:
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx |
|---|---|---|---|---|---|---|
| same-5 | 186h/5 | 186h/5 | icmp 0/10 · tcp FAIL · udp 0/200 | icmp 0/10 · tcp FAIL · udp 200/200 | 0 | 0 |
| cross-ap | 189/5 | 660a/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
| cross-ap | 660a/5 | 186h/5 | icmp 0/10 · tcp FAIL · udp 0/200 | icmp 0/10 · tcp FAIL · udp 200/200 | 0 | 0 |
| cross-ap | 186h/5 | 188n/5 | icmp 0/10 · tcp FAIL · udp 0/200 | icmp 0/10 · tcp FAIL · udp 0/200 | 0 | 0 |
| cross-ap | 188n/5 | 189/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 |
Section 6's run clears each sensor's MAC from every other AP once both have joined. With that, 186h ↔ 188n passes in both directions, with one ghost cleared before each case.
A (s1) joined the 188n on 5 GHz, then roamed with a plain reassociation to the 186h on the same channel and stayed there for 10 minutes, pinging the rXg once a second. B (EAP111) stayed on the 188n's 5 GHz BSS and pinged A once a second. Every 10 s the 188n was asked whether hostapd still listed A, how long A had been inactive, and how fresh sdan's entry for A was.
| Secs after roam | 188n lists A | A inactive (ms) | 188n sdan age (s) | 186h lists A | A → rXg | B → A |
|---|---|---|---|---|---|---|
| 10 | yes | 190 | 24 | yes | 9/9 | 0/9 |
| 60 | yes | 180 | 74 | yes | 10/10 | 0/10 |
| 120 | yes | 180 | 134 | yes | 10/10 | 0/10 |
| 180 | yes | 160 | 194 | yes | 10/10 | 0/10 |
| 240 | yes | 160 | 254 | yes | 10/10 | 0/10 |
| 300 | yes | 130 | 314 | yes | 10/10 | 0/10 |
| 350 | yes | 140 | aged out | yes | 10/10 | 0/10 |
| 360 | yes | 130 | aged out | yes | 10/10 | 0/10 |
| 420 | yes | 120 | aged out | yes | 10/10 | 0/10 |
| 480 | yes | 110 | aged out | yes | 10/10 | 0/10 |
| 540 | yes | 110 | aged out | yes | 10/10 | 0/10 |
| 600 | yes | 90 | aged out | yes | 10/10 | 0/10 |
A → rXg 601/601, B → A 0/601 over the whole run. The ghost's inactive time stayed between 90 and 200 ms throughout, because the 188n's radio counts A's frames to the 186h as A's activity, so hostapd's ap_max_inactivity=300 poll never fires. sdan's entry for A on the 188n aged out at about 350 s and B → A stayed dead, so the black hole is mac80211 on the old AP delivering to the ghost, not sdan.
Impact: a client that roams between APs on the same channel can't be reached by devices on the old AP's BSS and VLAN until it changes channel or roams back — in an MDU, a phone that moved APs and a TV or printer in the same unit. Its own Internet traffic is unaffected. Nothing in the rXg or rg_wss_client drops the client from the old AP; the likely fix is the rXg sending hostapd del_client to other APs when a station associates.
Upstream hostapd e17107912 (what we ship) and the QSDK 5.4 mac80211.
On a different channel the client isn't there to ACK, the poll fails, and the ghost drops after about 5 minutes. hostapd can't tell its own station from one associated to a co-channel neighbour, so the fix belongs above it: the rXg telling the old AP to del_client a station that associated elsewhere (rgnets/rxg#4495).
Live swap during the client rollout. The WF189 and WF186h were still on 4.2.4 when the 1.0.41 client reached them, so each swapped its loaded 1.16 module for 1.20 without a reboot. The rXg's syslog has the lines; the EAP111's came from its own log.
| AP | Platform / kernel | Release sent | Build fetched | Module old → new | Logged |
|---|---|---|---|---|---|
| 189 | ipq53xx, 6.1.31 | 23.05 | ipq53xx/generic/6.1.31/23.05 | 815d30fea899 → 7fde04f8a7e6 | unloaded rxg_sdan — loading new module |
| 186h | ipq50xx, 5.4.164 | 23.05 | ipq50xx/generic/5.4.164/23.05 | 59e9af82d6ef → 4ea03ee6b30c | unloaded rxg_sdan — loading new module |
| eap111 | mediatek/filogic, 6.6.89 | 23.05 | mediatek/filogic/6.6.89/23.05 | 510d83977bfc → cb9442f5a94f | unloaded rxg_sdan — loading new module |
Every new hash matches the file the rXg serves. The WF189 and WF186h stayed online on 1.20 for about 75 minutes, until the profile moved them to 5.1.
WF660a and WF186h moved back to 4.2.4. After the profile pointed them at 4.2.4-0.011 again, the WF660a loaded ipq60xx/generic 5.4.164 23.05 (31cf65c751ce) and the WF186h ipq50xx/generic 5.4.164 23.05 (4ea03ee6b30c), both matching what the rXg serves.
| AP | Band | MHz | BSSID | Lease | Ping |
|---|---|---|---|---|---|
| 660a | 5 | 5745 | d4:ba:ba:a1:8c:01 | 10.20.2.3 | 15/15 |
| 186h | 5 | 5785 | d4:ba:ba:a4:05:cb | 10.20.2.3 | 15/15 |
| 660a | 2.4 | 2437 | d4:ba:ba:a1:8c:02 | 10.20.2.4 | 15/15 |
| 186h | 2.4 | 2462 | d4:ba:ba:a4:05:ca | 10.20.2.4 | 15/15 |
| Case | A | B | A → B | B → A | local_sw pkts | tunnel_tx | ghosts cleared |
|---|---|---|---|---|---|---|---|
| pair | 660a/5 | 660a/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 4373 | 0 | 2 |
| pair | 186h/5 | 186h/2.4 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 4293 | 0 | 1 |
| pair | 660a/5 | 189/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 | 0 |
| pair | 186h/5 | 188n/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 | 1 |
| pair | 188n/5 | 186h/5 | icmp 10/10 · tcp ok · udp 200/200 | icmp 10/10 · tcp ok · udp 200/200 | 0 | 0 | 1 |
| pair | 660a/5 | 660a/5 | no-assoc: exception | ||||
Cross-band on each 4.2.4 AP was switched on the AP. 660a ↔ 189 is 4.2.4 ↔ 5.1. The last case failed in the harness: sensord raised an exception before the sensor associated, so it says nothing about the AP, and same-band cases don't exercise sdan anyway.
Sensors run with rgminiloader stopped so the rXg cannot reconfigure them. s1's tri-band MLO radio is split into per-band radios (ath12k mlo_capable=0); the EAP111 needs no split. Each client sits in its own VRF via rg_wss_client's VrfDomain, so a reply proves the path through that association. Targets are read live from each AP's iw dev. Harness: ~/lab/sensor/sweep.py, pair.py and ghost.py driving sensord.py; raw results in runs/sweep-20261014-*, runs/pair-20261014-* and runs/ghost-20261014-061639.