OpenWiFi 5.1 on rxg_sdan 1.20 — retest for !5593

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.

Bottom line

Versions under test

ComponentVersion
rXg16.827-9-hbhowksdan51update · branch hbh/ow-ksdan-5-1-update @ 9f7f7567b3
rxg_sdan bundle served by the rXg1.20-ac064a8 (module and rxg_sdanctl report 1.20, commit ac064a82)
AP firmware, 5.1 APsApNos-92cbcec63 · wlan-ap rgnets-5.1.0-0.006 · OpenWrt 25.12.3 r32912-6639b15f62
AP firmware, 4.2.4 APsTIP-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

1. sdan 1.20 on each AP

APModelFirmwareBuild keyHow 1.20 arrivedModule SHA-256
189CIG WF1895.1.0-0.006ipq53xx/generic 6.6.88 25.12fresh flash58e96a22c08c
660aCigtech WF-660a5.1.0-0.006ipq60xx/generic 5.4.164 25.12hot swap, no reboot (from 4779cd2071db)dcbf09ef9406
186hCIG WF186h5.1.0-0.006ipq50xx/generic 5.4.164 25.12fresh flasha5e8d323dc06
188nCigtech WF-188n5.1.0-0.006ipq60xx/generic 5.4.164 25.12hot swap, no reboot (from 4779cd2071db)dcbf09ef9406
eap111Edgecore EAP1114.2.4-0.011mediatek/filogic 6.6.89 23.05hot 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.

2. North-south: every BSSID — 11/11

AP5 GHz2.4 GHz6 GHz
189pass · ch 44 · 15/15pass · ch 6 · 15/15pass · ch 65 · 15/15
660apass · ch 149 · 15/15pass · ch 6 · 15/15no 6 GHz radio
186hpass · ch 157 · 15/15pass · ch 11 · 15/15no 6 GHz radio
188npass · ch 157 · 15/15pass · ch 6 · 15/15no 6 GHz radio
eap111pass · ch 149 · 15/15pass · ch 11 · 15/15no 6 GHz radio
APBandMHzBSSIDSSID / authLeasePing
18955220d4:ba:ba:a3:15:abOW mPSK WPA2 · WPA2-PSK10.20.2.315/15
660a55745d4:ba:ba:a1:8c:01OW mPSK WPA2 · WPA2-PSK10.20.2.315/15
186h55785d4:ba:ba:a4:05:cbOW mPSK WPA2 · WPA2-PSK10.20.2.315/15
188n55785d4:ba:ba:ab:ba:aaOW mPSK WPA2 · WPA2-PSK10.20.2.315/15
eap1115574522:1a:e3:db:53:68OW mPSK WPA2 · WPA2-PSK10.20.2.315/15
1892.42437d4:ba:ba:a3:15:aaOW mPSK WPA2 · WPA2-PSK10.20.2.415/15
660a2.42437d4:ba:ba:a1:8c:02OW mPSK WPA2 · WPA2-PSK10.20.2.415/15
186h2.42462d4:ba:ba:a4:05:caOW mPSK WPA2 · WPA2-PSK10.20.2.415/15
188n2.42437d4:ba:ba:ab:ba:abOW mPSK WPA2 · WPA2-PSK10.20.2.415/15
eap1112.42462a0:1a:e3:db:53:68OW mPSK WPA2 · WPA2-PSK10.20.2.415/15
18966275d6:ba:ba:a3:15:acOW mPSK WPA3 6GHz · WPA3-SAE (H2E, PMF)10.20.2.515/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.

3. Client-to-client — 16/17 on the first pass

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.

CaseABA → BB → Alocal_sw pktstunnel_tx
same-2.4189/2.4189/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
same-2.4660a/2.4660a/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
same-2.4186h/2.4186h/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
same-2.4188n/2.4188n/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
cross-band189/5189/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20044190
cross-band660a/5660a/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20044630
cross-band186h/5186h/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20041440
cross-band188n/5188n/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20043930
same-5189/5189/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
same-5660a/5660a/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
same-5186h/5186h/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
same-5188n/5188n/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
cross-ap189/5660a/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
cross-ap660a/5186h/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
cross-ap186h/5188n/5icmp 0/10 · tcp FAIL · udp 0/200icmp 0/10 · tcp FAIL · udp 0/20000
cross-ap188n/5189/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
cross-band-6189/6189/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20036610

4. The cross-AP failures are ghost stations

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

CaseABA → BB → Alocal_sw pktstunnel_tx
cross-ap186h/5188n/5icmp 0/10 · tcp FAIL · udp 0/200icmp 0/10 · tcp FAIL · udp 0/20000
cross-ap188n/5186h/5icmp 0/10 · tcp FAIL · udp 0/200icmp 0/10 · tcp FAIL · udp 0/20000

186h ↔ 189, both orders

CaseABA → BB → Alocal_sw pktstunnel_tx
cross-ap189/5186h/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
cross-ap186h/5189/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000

188n ↔ 660a, both orders

CaseABA → BB → Alocal_sw pktstunnel_tx
cross-ap660a/5188n/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 196/20000
cross-ap188n/5660a/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000

186h ↔ 188n, other band combinations

CaseABA → BB → Alocal_sw pktstunnel_tx
pair186h/2.4188n/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
pair186h/5188n/2.4icmp 0/10 · tcp FAIL · udp 400/200icmp 10/10 · tcp ok · udp 200/200130
pair186h/2.4188n/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000

186h same-5 alone, after the re-run

CaseABA → BB → Alocal_sw pktstunnel_tx
pair186h/5186h/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000

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:

CaseABA → BB → Alocal_sw pktstunnel_tx
same-5186h/5186h/5icmp 0/10 · tcp FAIL · udp 0/200icmp 0/10 · tcp FAIL · udp 200/20000
cross-ap189/5660a/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
cross-ap660a/5186h/5icmp 0/10 · tcp FAIL · udp 0/200icmp 0/10 · tcp FAIL · udp 200/20000
cross-ap186h/5188n/5icmp 0/10 · tcp FAIL · udp 0/200icmp 0/10 · tcp FAIL · udp 0/20000
cross-ap188n/5189/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/20000
The 660a ↔ 186h failure in the re-run fits the same cause (B on the 186h with a ghost on the 188n), but it was not traced as fully as 186h ↔ 188n. Observed separately: the old AP sometimes forwarded the roamed client's frames upstream as duplicates (one case received 400 of 200 UDP datagrams).

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.

5. Ghost persistence — never ages out on a shared channel

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 roam188n lists AA inactive (ms)188n sdan age (s)186h lists AA → rXgB → A
10yes19024yes9/90/9
60yes18074yes10/100/10
120yes180134yes10/100/10
180yes160194yes10/100/10
240yes160254yes10/100/10
300yes130314yes10/100/10
350yes140aged outyes10/100/10
360yes130aged outyes10/100/10
420yes120aged outyes10/100/10
480yes110aged outyes10/100/10
540yes110aged outyes10/100/10
600yes90aged outyes10/100/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.

Why hostapd never drops it

Upstream hostapd e17107912 (what we ship) and the QSDK 5.4 mac80211.

  1. Nothing tells the old AP the client left. A roam is a reassociation to the new AP. hostapd's only cleanup for a client that moved, hostapd_prune_associations(), walks the BSSes in the same hostapd process (other radios on the same AP), never other APs. IAPP (802.11F), which made the old AP drop a roamed client, was removed in hostapd 018edec9b (2019).
  2. The inactivity timer's checks both pass on a bare 802.11 ACK. ap_handle_timer() reads the kernel's inactive time: mac80211's ieee80211_sta_last_active(), the later of last_rx and last_ack. Every frame the old AP forwards to the ghost and gets ACKed resets it, so the station never looks idle. After 300 s of real idleness hostapd sends a QoS-null poll, and an ACK clears PENDING_POLL and keeps the station for another 300 s. A radio ACKs every unicast frame addressed to its MAC, whichever BSS it's associated with, so a client parked on the same channel keeps the ghost alive indefinitely.
  3. The client never refuses. It drops the old AP's frames silently instead of answering with a deauth.

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).

6. 4.2.4 on QSDK APs

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.

APPlatform / kernelRelease sentBuild fetchedModule old → newLogged
189ipq53xx, 6.1.3123.05ipq53xx/generic/6.1.31/23.05815d30fea899 → 7fde04f8a7e6unloaded rxg_sdan — loading new module
186hipq50xx, 5.4.16423.05ipq50xx/generic/5.4.164/23.0559e9af82d6ef → 4ea03ee6b30cunloaded rxg_sdan — loading new module
eap111mediatek/filogic, 6.6.8923.05mediatek/filogic/6.6.89/23.05510d83977bfc → cb9442f5a94funloaded 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.

APBandMHzBSSIDLeasePing
660a55745d4:ba:ba:a1:8c:0110.20.2.315/15
186h55785d4:ba:ba:a4:05:cb10.20.2.315/15
660a2.42437d4:ba:ba:a1:8c:0210.20.2.415/15
186h2.42462d4:ba:ba:a4:05:ca10.20.2.415/15
CaseABA → BB → Alocal_sw pktstunnel_txghosts cleared
pair660a/5660a/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/200437302
pair186h/5186h/2.4icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/200429301
pair660a/5189/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/200000
pair186h/5188n/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/200001
pair188n/5186h/5icmp 10/10 · tcp ok · udp 200/200icmp 10/10 · tcp ok · udp 200/200001
pair660a/5660a/5no-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.

Scope

Method

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.