Testlab runbook¶
Problems caused on purpose at home, so the right outcome is known in advance: per scenario the steps, the commands and the expected verdict per question. One cause at a time, against a baseline without it, with the expected verdict written down before the recording. A recording becomes test data with its outcome in test data; what is still to record is in the plan (docs/status.md).
Setup¶
| Role | Device |
|---|---|
| The AP45 | Mist AP45 in the work org, on a test site of its own, in the office on a chair: 2.4, 5 and 6 GHz up to 160 MHz, no MLO, a fourth radio that scans. Mist's stats, events and AP pcap, and the campus's 802.1X path. The fourth radio can send synthetic client frames (Marvis Minis), with a Juniper OUI |
| The second AP45 | Mist AP45 on the same test site, in the living room next to the kitchen on a chair, on the same firmware |
| AP A | UniFi U7 Pro Max, in the office, for Wi-Fi 7 and 6 GHz: the home SSID on 5 GHz ch 48/80 and 6 GHz ch 37/160 (on 2026-10-20 at ch 85/320: back to 37/160 for drops), the IoT SSID (WPA2) on 2.4 GHz ch 1, linkside (WPA3) on 6 GHz. The 5 GHz channel fixed, not Auto |
| AP B | UniFi U7 Pro Max, in the living room next to the kitchen, for Wi-Fi 7 and 6 GHz: the home SSID on 5 GHz ch 40/80 and 6 GHz ch 53/320, the IoT SSID on 2.4 GHz ch 6 |
| Scripted client | Stick B on the Fedora PC, in its own network namespace with wpa_supplicant, at a fixed address |
| iperf3 server | The Fedora PC on its Ethernet port |
| Phone | Own phone with its private address: the client of the roaming scenarios, the reference client or the load of the others |
| Sensor | The Pi with stick A as capA; with stick B as well when the phone is the client |
| Mac | On Ethernet to the Pi; its Wi-Fi on a band the sensor does not listen on, awdl0 down, Bluetooth off for 2.4 GHz |
The two AP45s carry every scenario but those of Wi-Fi 7 and 6 GHz, which stay on the U7s; the U7s run the home network as well, the Mac's IoT SSID among it. Both AP45s broadcast eduroam, the campus's WLAN, and testwlan-1, a WLAN of the site's own, bridged untagged onto the home LAN so its clients reach the Fedora PC: WPA3-SAE with WPA2-PSK in transition mode, Wi-Fi 7 Security off as on eduroam, 802.11r, on 2.4 and 5 GHz. Its beacons offer PSK, FT-PSK, PSK-SHA256, SAE and FT-SAE (AKMs 2, 4, 6, 8 and 9) with CCMP, PMF capable and not required, and one Mobility Domain on both AP45s, FT over the air only (2026-11-03). Its DHCP is the home router's, so a failed DHCP or a scope with a wrong gateway needs a VLAN of its own with a DHCP server on the Fedora PC. Beside it, from 2026-11-03, testwlan-psk, also untagged on the home LAN: WPA2-PSK alone, PMF off and no 802.11r, so every reason is readable in the air, on 2.4 and 5 GHz of the AP45 alone (in Mist the WLAN applies to that AP), for the held-out recordings. A wrong password for the iPhone or the Mac comes from the WLAN's passphrase changed in Mist after the client joined with the right one, its Auto-Join off: Forget This Network gives the iPhone another private address on its next join. After two failures on a BSS Mist puts the client in its penalty box, which holds its next attempts there (mist-events.md).
Mist's RRM sets the AP45s' channels and power within the site's RF template: 2.4 GHz on ch 11 alone, apart from the U7s' 1 and 6; 5 GHz at 20 MHz on 52 to 140, 36 to 48 left to the U7s; 6 GHz at 80 MHz as RRM chooses. On 2026-11-03 the AP45 was on 5 GHz ch 132 and the second AP45 on ch 108, on 6 GHz on ch 5/80 and 69/80, the first inside AP B's 320 MHz (ch 1 to 61, testlab-aps.md). On ch 11 Mist read 18 % non-Wi-Fi utilization at the AP45 in the office against 7 % at the second AP45 in the living room, so a scenario on 2.4 GHz goes in the living room; the house has Philips Hue, which runs Zigbee. Each AP45 stays on its chair in the same position between sessions: a moved AP changes its signal at the sensor. Their MAC addresses and BSSIDs come from Mist (linkside-cli mist aps) and from the context scan, not from this page. On an AP45's page in Mist the 2.4 GHz radio is Dual Band Radio Config: Enable on a value of its own opens Band, Channel Width, Channel and Power, and "Use site setting" gives it back to RRM; on 2026-11-04 ch 6 set there came up at 17 dBm against RRM's 11.
Two setups follow from the two sticks:
- Fedora client, one radio: stick B on the Fedora, the sensor runs
capAonly. NocapB, so no roam target and no context sweep: other APs are heard only during the discovery before the lock, and the focus filter takes the serving AP's beacons alone. New Session says "ready, capA only". - Phone client, two radios: both sticks in the Pi,
capBon the roam target chosen in New Session.
Before every session¶
linkside-radio statuson the Pi (orstatus capAwith one radio): every checkok=1. For a focus on 2.4 GHz, stick A on the USB 2 port of the same controller (speed=480).- Mac Wi-Fi off the channel of the focus: the IoT SSID (2.4 GHz only, ch 1 and 6), which overlaps none of the AP45s' channels, ch 11 included, nor a focus on 5 or 6 GHz; the home SSID for a focus on a U7's 2.4 GHz. The home SSID is on 5 and 6 GHz and macOS does not let you choose the band, so the Mac can land on the measured channel.
sudo ifconfig awdl0 downafter joining that network, since macOS bringsawdl0back with every change of network: AirDrop off in System Settings leavesawdl0active, on ch 44 inside the 80 MHz of ch 48;ifconfig awdl0should showstatus: inactive. Bluetooth off for 2.4 GHz. Sensor about 1 m from the client. - The channels as they are. The AP45s' from Mist,
linkside-cli mist aps --site <site-id>with the API URL and the org inLINKSIDE_MIST_APIandLINKSIDE_MIST_ORG: RRM can move them, overnight or after radar on a DFS channel, and thefreq_listof the configs ontestwlan-1follows them. The U7s on the channels of the setup table: UniFi can move a radio, and a config pinned to 5240 then finds nothing andwpa_supplicantstays silent (AP A's 5 GHz was on ch 44 on 2026-10-20). - Note in the session notes: AP settings (channel, width, transmit power, security and PMF, 802.11r, BSS transition), the AP45s' Mist firmware, UniFi Network version and the U7s' firmware, phone OS, the Fedora kernel and mt7925 firmware.
- Note the clock time at every step of the scenario (start, problem on, problem off): the hint's window or moment should match it.
The scripted client¶
Once per boot of the Fedora PC, with wlan the interface of stick B (wlp0s20f0u8u4) and <client-mac> a fixed locally administered address, which is in the keep list of tools/anonymize:
sudo nmcli device set wlan managed no
phy=$(basename "$(readlink /sys/class/net/wlan/phy80211)")
sudo ip netns add wifi
sudo iw phy "$phy" set netns name wifi
sudo ip netns exec wifi rfkill -o ID,TYPE,DEVICE,SOFT # the ID of the stick's phy
sudo ip netns exec wifi rfkill unblock <id>
sudo ip -n wifi link set wlan address <client-mac>
sudo ip -n wifi link set lo up
sudo ip -n wifi link set wlan up
sudo ip -n wifi addr add <wifi-ip>/24 dev wlan # a free address of the home LAN, outside the DHCP pool
NetworkManager's Wi-Fi radio is off on the Fedora, so it soft-blocks every new Wi-Fi phy. rfkill reads names and IDs from sysfs, and the phy's sysfs is only in the namespace: rfkill outside it shows the wrong names and refuses the ID. The internal Wi-Fi stays blocked.
testwlan-1 and the U7s' SSIDs bridge to the home LAN, so the namespace reaches the Fedora's Ethernet address without a route, and the traffic between them goes over the air through the AP. sudo ip netns del wifi gives the stick back.
Run the four terminals (wpa_supplicant, two iperf3 servers, the traffic) in one tmux session on the Fedora: a change of the Mac's Wi-Fi drops its SSH sessions, and wpa_supplicant survives that half. Afterwards the panes go to files with tmux capture-pane -p -S - -t lab:<n> > pane-<n>.txt, for the session notes.
One wpa_supplicant file per variant, in ~/linkside-lab/ on the Fedora:
# psk.conf: WPA2-PSK without PMF on testwlan-1 (transition mode allows it), on the AP45's 5 GHz channel
network={
ssid="testwlan-1"
key_mgmt=WPA-PSK
psk="<password>"
ieee80211w=0
freq_list=<MHz>
scan_freq=<MHz>
}
# sae.conf: WPA3-SAE, PMF required
network={
ssid="testwlan-1"
key_mgmt=SAE
sae_password="<password>"
ieee80211w=2
}
<MHz> is the AP45's 5 GHz channel from Mist, 5000 + 5 × the channel: 5660 for ch 132. Variants: psk-wrong.conf and sae-wrong.conf with a wrong password; ft-psk.conf and ft-sae.conf with key_mgmt=FT-PSK and FT-SAE, for 802.11r between the two AP45s; psk24.conf on 2.4 GHz (freq_list=2462, ch 11); freq_list or bssid=<BSSID> pins the AP, otherwise the client may pick the other AP45; sae_groups=21 in sae-group.conf. On the U7s the same files with the home SSID and freq_list=5240 for AP A's ch 48, and iot.conf for the IoT SSID on 2.4 GHz (WPA-PSK, ieee80211w=0).
sudo ip netns exec wifi wpa_supplicant -D nl80211 -i wlan -c ~/linkside-lab/psk.conf # Ctrl-C leaves (deauth, reason 3)
iperf3 -s -B <fedora-eth-ip> # server, in another terminal
iperf3 -s -B <fedora-eth-ip> -p 5202 # second server, for the downlink
sudo ip netns exec wifi sh -c 'iperf3 -c <fedora-eth-ip> -u -b 2M -t 60 & iperf3 -c <fedora-eth-ip> -p 5202 -u -b 2M -t 60 -R; wait' # traffic both ways
sudo ip netns exec wifi iw dev wlan station dump # tx retries and tx failed of the client
UDP both ways gives Block Acks in both directions: without them a direction is "too little traffic" and its rules stay untested. TCP downlink alone gives few uplink Block Acks. iperf3 --bidir with UDP ends at once on the Fedora ("unable to read from stream socket"), so each direction has its own client and server. Below, "both ways" means this command. At 2 Mbit/s each way the AP and the client still send some 170 Block Acks a second each, and a minute of recording stays a few MB; at 30 Mbit/s four minutes were 200 MB and took minutes to replay. Only drops and ba-validation need a high rate.
The scripted client on eduroam¶
The eduroam scenarios (eduroam-login, eduroam-cert-refused, eduroam-wrong-password) put stick B on the AP45's eduroam with the developer's own account, by PEAP or TTLS, beside Mist: the campus's 802.1X path and Mist's reason for a login that fails. A wrong password counts toward the account's lockout at the identity provider. wpa_supplicant therefore makes one attempt per run and is stopped before it tries again, and a refused certificate sends no credentials: of all the runs below, only eduroam-wrong-password costs a failed login. Everything runs on the Fedora over SSH from the Mac, in the tmux session, with the Mac's Wi-Fi on the IoT SSID.
Every run is one command, with its log beside the files. Ctrl-C at the first CTRL-EVENT-EAP-FAILURE: wpa_supplicant waits 10 s before it tries again (CTRL-EVENT-SSID-TEMP-DISABLED, duration=10), then 20 and 30 s. tee -i keeps the last lines through the Ctrl-C, and the count afterwards is the attempts, 1:
sudo ip netns exec wifi wpa_supplicant -D nl80211 -i wlan -c ~/linkside-lab/<file>.conf -t | tee -i ~/linkside-lab/<date>-<run>.log
grep -c CTRL-EVENT-EAP-STARTED ~/linkside-lab/<date>-<run>.log
Once, before the scenarios:
- Stick B in the namespace as above, then without the home LAN's address,
sudo ip -n wifi addr flush dev wlan: eduroam leaves at the campus, where Mist takes an address of the home LAN for an invalid client IP. - The AP45 online in Mist, its 5 GHz channel from Mist (
linkside-cli mist aps), and its eduroam BSSID on that channel from a scan of stick B, which is the client and no radio of the sensor:sudo ip netns exec wifi iw dev wlan scan freq <MHz> | awk '/^BSS/ {b = substr($2, 1, 17)} /SSID: eduroam$/ {print b}'. - The three files below in
~/linkside-lab/.eduroam.confholds the account's password:chmod 600, and removed witheduroam-wrong.confafter the scenarios.key_mgmtwithout FT, so a PMKID in a (re)association is PMK caching's; with FT it is the PMKR1Name. - The server's name, without a login:
eduroam-certfail.confonce, not recorded.CTRL-EVENT-EAP-METHODnames the method the server took.wpa_supplicantprints the server's chain,CTRL-EVENT-EAP-PEER-CERTper depth with the server's DNS names inCTRL-EVENT-EAP-PEER-ALT, and inCTRL-EVENT-EAP-TLS-CERT-ERRORwhat it refused: the name, with a chain the system's bundle trusts, or the issuer, with a CA of the institution's own, which its eduroam profile carries. Intoeduroam.confandeduroam-wrong.confgodomain_match, the server's DNS name, andca_cert, the bundle or that CA. A server that takes EAP-TLS alone proposes method 13 (CTRL-EVENT-EAP-PROPOSED-METHOD) and ends in an EAP-Failure before any certificate: the account has no password method, and the scenarios do not run. - A login with the right password, not recorded:
eduroam.conf, Ctrl-C afterCTRL-EVENT-EAP-SUCCESS. It shows that the campus takes the account by password, and Mist knows the client and its AP from then on, in the search a minute or two later: New Session takes its channel and BSSID from Mist, and the sessions below lock at once, where a client that only tries to log in is hard for the sweep to find. The 4-way after it can fail on the stick, which misses M3 after its M2 (testlab-baseline.md); the EAP-Success is what counts. An EAP-Failure here is a password typed wrong or an account the server does not take by password: one attempt, counted, and no second.
# eduroam-certfail.conf: a server name that is not the server's, and an identity that is no account
ctrl_interface=/run/wpa_supplicant
network={
ssid="eduroam"
key_mgmt=WPA-EAP-SHA256 WPA-EAP
ieee80211w=1
eap=PEAP TTLS
anonymous_identity="anonymous@<realm>"
identity="linkside-nobody@<realm>"
password="none"
phase2="auth=MSCHAPV2"
ca_cert="/etc/pki/tls/certs/ca-bundle.crt"
domain_match="radius.invalid"
bssid=<BSSID>
freq_list=<MHz>
scan_freq=<MHz>
}
# eduroam.conf: the same with identity="<user>@<realm>", the account's password, and domain_match and ca_cert from step 4
# eduroam-wrong.conf: eduroam.conf with a wrong password
Recording¶
- App: New Session, the client's MAC address, and for the roaming scenarios the roam target (the second AP45's radio, on the U7s AP B's). Stop, then Export Report and Capture into the same folder.
- CLI:
linkside-cli live --client <mac> [--reference <mac>] --record ~/linkside-captures/<date>-<scenario> --seconds <n>. - Name the folder
<date>-<scenario>, as in the headings below.
Held out¶
The scenarios recorded again with other clients, without a rule change in between, so the scorecard gets rows that test the rules instead of having shaped them. The expected verdict is written down before the recording; one that comes out wrong is a finding for expected.json as a mismatch and a row in the scorecard, not a reason to tune on the spot. Recorded on 2026-11-03 and 2026-11-04 with the iPhone and the MacBook: twenty-one sessions, testlab-wrong-psk-psk-iphone and the twenty after it in the scorecard.
The clients on eduroam. The iPhone and the Mac, from their recordings of 2026-11-02 on the AP45 (mist-events.md): both take FT-802.1X (AKM 3) with CCMP and PMF, log in with EAP-TLS, and join again with a full EAP-TLS, without a PMKID: no PMK caching. Each client keeps one private address per network for the recordings (iOS and macOS "Fixed"), noted per network before its first recording.
The Mac as the client: its Wi-Fi joins the measured WLAN, and the chat and Mist's polls run over it. With its Wi-Fi off (client-leaves) or its password wrong (wrong-psk) the Mac is offline for those minutes: the chat waits, and a failed poll is no fatal error, so the poller goes on, and its next poll asks for the events of the last 15 minutes. The Mac is back online before Stop, for the minute of polls after it.
The download held to 2 Mbit/s: a shaper on the Fedora PC's Ethernet (enp7s0) for the web server's port alone, so iperf3's load passes at full rate:
sudo tc qdisc add dev enp7s0 root handle 1: htb default 10
sudo tc class add dev enp7s0 parent 1: classid 1:10 htb rate 1gbit
sudo tc class add dev enp7s0 parent 1: classid 1:20 htb rate 2mbit
sudo tc filter add dev enp7s0 parent 1: protocol ip u32 match ip sport 8000 0xffff flowid 1:20
sudo tc qdisc del dev enp7s0 root # afterwards
Expected per recording, for the phone and the Mac alike. Notes beside the finding, such as the frames sent again after the phone's wakes, are no mismatch.
| Recording | Expected | Rule |
|---|---|---|
wrong-psk on testwlan-psk |
client side, "Login failed": M2 without M3, then the AP's deauthentication with its reason readable; the reference client connects | credentials |
wrong-psk on testwlan-1 |
client side, "Login failed": the client's SAE confirm refused or left unanswered; the reference client connects | credentials |
client-leaves on testwlan-psk |
client side, "The client left": its disassociation (8) or deauthentication (3) in the clear; "The client went silent" where it leaves without a frame | client-disconnect; client-silent |
client-leaves on testwlan-1 |
client side, "The client left": its disassociation encrypted, Mist's reason 8 (CLIENT_DEASSOCIATION) within 3 s. Without Mist's reason the note "The client left (PMF)" and undecided, as testlab-client-leaves-sae without Mist |
client-disconnect; encrypted-disconnect |
| client-leaves on eduroam | client side, "The client left": the encrypted logoff, then the client's protected disassociation with Mist's reason 8, or the AP's 23 or 36 in the clear | client-disconnect |
ap-kick on testwlan-psk |
AP side, "Disconnected by the AP", 2 times, reason 2 in the clear | ap-disconnect |
| ap-kick on eduroam | AP side, "Disconnected by the AP", 2 times, the deauthentication protected, reason 2 from Mist | ap-disconnect |
upstream on testwlan-1 |
upstream in expected.json; the app says "Nothing abnormal found", or "No finding" beside a note, with BA success Good both ways, the mismatch of every upstream recording, as no rule names a cause beyond a clean link. The test is that no rule of the air fires |
– |
co-channel on testwlan-1 |
RF environment, "Busy channel" over the load, by the AP45's QBSS: with the retries where the client sends enough to have them, on the load alone where it does not; no finding before or after it | congestion |
intermittent-loss on testwlan-psk, 2.4 GHz, the phone |
RF environment, "Losses towards the client" in the oven's window; "Losses both ways" where the oven reaches the AP45 as well | downlink-loss; two-way-loss |
Afterwards¶
- Compare the app with the expected result below; note what differs and why. Change no rule yet.
- Anonymize as the earlier sessions: the sources, the keep list, the mapping and the exact command are in
~/linkside-captures/testdata-src/README.txt, outside the repo. Add the new session at the end of that one run, so the addresses of the earlier sessions stay byte for byte the same; a new own address goes into the keep list. For a new client, check the fields the mapping does not cover: the UUID-E of WPS and the device names in WPS and P2P (anonymize). - Write the session's
expected.jsonfrom the expected result below (testdata). Inapp/LinksideKit:LINKSIDE_UPDATE_GOLDEN=1 swift test --filter GoldenTests, review the newgolden.jsonandverdict.json, and add the session totestdata/README.mdand its row to the scorecard: itsscoreline is "as recorded". - Only then a rule change, preferably after a few recordings that point the same way; the
scorelines of all recordings show what it breaks.
Fedora client, one radio¶
baseline¶
Stick B on the AP45, testwlan-1 on 5 GHz, psk.conf pinned to its channel. One minute of UDP at 2 Mbit/s both ways, then a reconnect (Ctrl-C, start again at once), then one minute more.
Expected: no clear cause, "Nothing abnormal found"; every readout Good, Coverage with Mist's client RSSI, "no roam" in the help of Roaming (the AP is strong, so the client need not move). On the time axis loss stays under the 3 % line, the AP lane flat, the MCS dark (captured). Note the BA success and the capture share per direction: the reference for the other scenarios.
Recorded on AP A, without Mist, on 2026-10-20 as testlab-baseline, with a run at 30 Mbit/s before it (testlab-baseline.md). Again on 2026-10-23 with the client's ACKs in the capture, five restarts of wpa_supplicant before the traffic, as testlab-4way-ack: the client misses M3 in four of nine 4-ways.
wrong-psk¶
Stick B disconnected before the session starts; in New Session the AP45's 5 GHz channel (width 20) and its BSSID of testwlan-1 typed in, since a client that never connects is slow to find. psk-wrong.conf on the AP45 for two minutes, with wpa_supplicant -t for timestamps; the phone on the same AP with the right password as reference client (--reference or New Session), Wi-Fi off and on once at the start. Then sae-wrong.conf for two minutes.
Expected: on the client side; Connection Poor, "Login failed" at the moment of the last stalled 4-way (M2 without M3, repeated, then a deauthentication with its reason). With SAE: "the AP did not accept the client's SAE confirm", or a confirm left unanswered. Both directions untested (no traffic). Note Mist's events for each, with their codes and delay.
Recorded on AP A on 2026-10-20 as testlab-wrong-psk. Held out on the AP45 on 2026-11-03 with the iPhone and the MacBook, on testwlan-psk and on testwlan-1: testlab-wrong-psk-psk-iphone and the three after it.
client-leaves¶
Two sessions, the channel and BSSID typed in as for wrong-psk. psk.conf: connect, one minute of traffic, then Ctrl-C and two minutes without the client. Then the same with sae.conf in a session of its own: in one session the PSK hint would decide the outcome.
Expected with PSK: on the client side, "The client left (reason 3)" at the clock time of the Ctrl-C. With SAE the deauthentication is protected: no finding, the note "The client left (PMF)" at the clock time of the Ctrl-C, and Connection Good with "reason encrypted (PMF)" in its help. That difference is the test.
Recorded on AP A on 2026-10-20 as testlab-client-leaves-psk and testlab-client-leaves-sae, and on 2026-10-21 on eduroam beside Mist as testlab-client-leaves-eduroam, with the phone as the client turning its Wi-Fi off. Held out on the AP45 on 2026-11-03 with the iPhone (Wi-Fi off in Settings) and the MacBook (in the menu bar, once in System Settings), on testwlan-psk, testwlan-1 and eduroam: testlab-client-leaves-psk-iphone and the six after it.
ap-kick¶
Two sessions, the channel and BSSID typed in. The kick: psk.conf connected (wpa_supplicant stays running and reconnects by itself), traffic, and Reconnect on the client's page in the Mist portal twice, a minute apart. The reboot: the same with five minutes of traffic, Reboot of the AP45 in the portal after a minute, and recording until the client is back plus a minute. Stop the session before wpa_supplicant: a Ctrl-C inside the recording adds "The client left". For the reboot psk.conf pins the AP45 with bssid= instead of freq_list, since RRM may bring it back on another channel: note the channel it came back on.
Expected for the kick: on the AP side, "Disconnected by the AP" with the reason, readable without PMF (Mist's Reconnect is reason 2). For the reboot: on the AP side, "The AP stopped beaconing" at the moment of the last beacon; the AP lane stops on the time axis.
Recorded on AP A, with UniFi's Reconnect (kick) and Restart, on 2026-10-20 as testlab-ap-kick and testlab-ap-reboot.
On the AP45 in the work org: the phone on eduroam with a video playing, and Reconnect on its page in the Mist portal. Recorded on 2026-10-21 as testlab-ap-kick-eduroam. Held out on 2026-11-03 with the iPhone and the MacBook, Reconnect twice, on testwlan-psk (the reason in the clear) and on eduroam: testlab-ap-kick-psk-iphone and the three after it.
intermittent-loss¶
On the U7s: the MacBook as the client needs a WLAN on 2.4 GHz alone, since macOS cannot pin a band, and testwlan-1 is on 2.4 and 5 GHz. The microwave oven is built into the kitchen, so the client goes there: the MacBook, on the IoT SSID, with the Pi and stick A (USB 2 port) next to it, both 1 m from the oven, and iperf3 from the Mac to the two servers on the Fedora. The Mac is the measured client here; the app's red warning about its Wi-Fi on the channel is expected. AP A, in the office, is the far AP: its 2.4 GHz on ch 6, AP B's 2.4 GHz radio off (transmit power disabled), since macOS cannot pin an AP. In New Session the Mac's Wi-Fi address, 2437, width 20 and AP A's IoT BSSID; no reference client. Ten minutes of UDP at 1 Mbit/s both ways (-t 600). At about five minutes, the oven with a cup of water on full power for 20 s (its minimum is a minute: stop it by hand). Afterwards AP A back to ch 1 and AP B's 2.4 GHz back on.
Expected: AP to client Poor with the dip at the clock time of the microwave, while the session mean stays near Good; "Losses towards the client" in the RF environment, found in the window of the microwave. Client to AP stays Good (the AP is far from the oven). On the time axis the AP to client line rises above the 10 % line for about 20 s. Also note the loss with the oven at 2 m and at 4 m.
Recorded on 2026-10-20 as testlab-intermittent-loss. AP A is not far enough from the oven for a clean uplink: the uplink fell in the oven's window too.
Held out on the AP45 on 2026-11-04 with the iPhone, 2 m from the oven: testlab-intermittent-loss-psk-iphone-moved and testlab-intermittent-loss-psk-iphone. The phone on testwlan-psk, set to 2.4 GHz alone in Mist for the session, on the AP45's 2.4 GHz on ch 6, a download of mid.bin held to 2 Mbit/s (held out), capA alone with stick A on the USB 2 port and stick B out of the Pi, as the SSID has no roam target on 2.4 GHz and USB 3 raises the noise floor. Auto-Lock on Never: the screen stays on without a hand on the phone. UniFi had the office U7's 2.4 GHz on ch 6 that day, not ch 1: the beacons on ch 6 checked before the recording, both U7s' 2.4 GHz off.
co-channel¶
On the U7s: two APs on one 5 GHz channel at 80 MHz, fixed, which RRM does not give the AP45s. AP B on the same channel as AP A (5 GHz ch 48/80, fixed). The phone in the living room next to AP B, on AP B: Wi-Fi off and on there, then check the AP in UniFi, since both APs are on ch 48 and an iPhone stays on the AP it had. The load is a download from the Fedora over the LAN, as fast as the channel allows; an iPhone has no iperf3, so Safari downloads a sparse file from a web server in its own tmux window:
mkdir -p ~/linkside-lab/www && truncate -s 8G ~/linkside-lab/www/big.bin && tmux new-window -d -t lab -n www 'python3 -m http.server 8000 --bind <fedora-eth-ip> --directory ~/linkside-lab/www'
Stick B on AP A with UDP at 2 Mbit/s both ways for six minutes (-t 360); from minute two to minute four the phone opens http://<fedora-eth-ip>:8000/big.bin and downloads it (8 GB is about two minutes; delete it on the phone afterwards). Afterwards AP B back to ch 40.
Expected: in the RF environment, "Busy channel" with the QBSS load of AP A in the window of the download: the co-channel count is at most 1 (AP B, heard during the discovery), under the 3 of the rule, so only QBSS can show the load. The loss stays near zero: both APs hear each other, so they share the channel by CSMA, with more retries and a lower MCS; a client whose own traffic stays light shows few retries, and the load alone carries the finding from 80 %.
Recorded on 2026-10-20 as testlab-co-channel. Held out on the AP45s on 2026-11-03, both on ch 132 at 20 MHz, set per AP in Mist: testlab-co-channel-mac and testlab-co-channel-iphone, the phone's download a file of 128 MB (truncate -s 128M ~/linkside-lab/www/mid.bin), as upstream says why. A roam target on capA's channel adds nothing: capB takes another band of the client's AP.
drops¶
On the U7s, for 160 MHz on 6 GHz. Stick B on AP A's linkside (6 GHz ch 37/160, sae.conf with that SSID); stick A on the USB 2 port at 160 MHz. Two minutes of TCP download at full rate: sudo ip netns exec wifi iperf3 -c <fedora-eth-ip> -R -t 120.
Expected: measurement limit with the finding "Capture not reliable"; the drops in linkside-cap and ip -s link on the Pi. Then the same on the USB 3 port: no drops, as the reference. What does not fit through USB 2 is lost before linkside-cap, in the radio or the driver (sensor-link.md): check both counters, and the capture share per direction.
Recorded on 2026-10-23, outside the repo (sensor-link.md): not the drops finding, since linkside-cap counted none on either port; on USB 2 more than half the client's Block Acks were lost before it, BA success read 93.0 % up against 96.9 % and the TXOPs failed. The iperf3 server needs a gigabit link: one of 100 Mbit/s held the load to 94 Mbit/s, which fits through USB 2.
ba-validation¶
Stick B on the AP45, testwlan-1 on 5 GHz (psk.conf). iw dev wlan station dump before and after each run of two minutes:
sudo ip netns exec wifi iperf3 -c <fedora-eth-ip> -u -b 50M -t 120(uplink), clean.- The same with the phone on the AP45 downloading next to it.
- On 2.4 GHz with
psk24.confand the microwave for the whole run (stick A on USB 2).
Expected: Client to AP BA success against the client's own counters, 1 − tx retries / tx packets over the run, per run. A large difference puts the primary metric up for review.
The A9000 as client counts no retries: tx retries and tx failed stay 0 in station dump after 62,000 packets while the sensor sees the retry bit on its frames, and the driver's tx_stats in debugfs hold only the A-MPDU lengths, with BA miss count 0. The uplink therefore needs an Intel AX210 or BE200 on Linux, once its station dump shows tx retries; the downlink takes the AP's counters, mca-dump on the U7 or Mist's tx_retries on the AP45. Per run BA success against the first-attempt success (1 − tx retries / tx packets) and against the final loss (tx failed, iperf3's lost datagrams). Note the AP's RTS and the client's BAs after them as well: the TXOP loss (txop-loss.md) is checked on the same runs.
upstream¶
Shaping on the Fedora's Ethernet, so only the downlink is limited: sudo tc qdisc add dev <eth> root tbf rate 1mbit burst 32kbit latency 400ms. Stick B on the AP45 (psk.conf) with UDP at 2 Mbit/s both ways for three minutes (-t 180): 2 Mbit/s offered each way, 1 Mbit/s of it through the shaper, the client's airtime low. Remove the shaping with sudo tc qdisc del dev <eth> root.
Expected: BA success Good both ways and "Nothing abnormal found": the sensor sees a clean link and cannot name what lies beyond it (no rule does, since wifi-fine was removed on 2026-10-26). Note the throughput and the loss iperf3 reports for the downlink: the loss is at the shaper, not on the air.
Recorded on AP A on 2026-10-20 as testlab-upstream, with enp7s0 as the Fedora's Ethernet. Held out on the AP45 on 2026-11-03 on testwlan-1: testlab-upstream-iphone and testlab-upstream-mac, the Mac with iperf3 -c <fedora-eth-ip> -u -b 2M -t 180 --bidir. An iPhone has no iperf3, so it downloads in Safari, a file of 32 MB beside big.bin (truncate -s 32M ~/linkside-lab/www/small.bin): behind the shaper it closed the connection of the 8 GB big.bin itself after 40 s, with the message "task failed", for a reason not found, and three minutes at 1 Mbit/s take about 20 MB.
sae-group¶
sae-group.conf (sae_groups=21) on the AP45.
Expected: an SAE commit refused with status 77, on the client side ("WPA3 fails for this client"), or a login with group 21 where the AP45 takes it: note which.
sae-group.conf is sae.conf with sae_groups=21 above the network block: a global option, not one of the network. Stick B disconnected first, the channel and BSSID typed in, as for wrong-psk. Two minutes; Stop in the app before the Ctrl-C.
Recorded on AP A on 2026-10-20 as testlab-sae-group: UniFi refuses group 21, and with no other group to offer wpa_supplicant sends one commit, not a series.
eduroam-login¶
The scripted client on eduroam (its preparation) with eduroam.conf. New Session with Mist: the AP45's site, the client by its MAC address, no device to compare with; wpa_supplicant started once the session listens on the AP45's channel. A minute connected, then sudo ip netns exec wifi wpa_cli -i wlan disconnect and 30 s later sudo ip netns exec wifi wpa_cli -i wlan reconnect; ten minutes on the same again, a minute after it Stop in the app, then Ctrl-C. wpa_supplicant keeps the PMK while it runs, and a restart would drop it.
Expected: no clear cause. In the timeline a full EAP (PEAP or TTLS) and the 4-way; after each reconnect a (re)association request with a PMKID, then "EAP skipped" and the 4-way where the AP45 takes the cached PMK, a full EAP where it does not. Note per reconnect the PMKID and what followed, the duration of each EAP, and Mist's events, CLIENT_ASSOCIATION_PMKC or its absence. The questions of traffic stay untested: the client has no address.
eduroam-cert-refused¶
eduroam-certfail.conf in a session as for eduroam-login, wpa_supplicant started once the session listens on the AP45's channel; Ctrl-C at the EAP-Failure. A minute more, then Stop, and the poller's minute after it.
Expected: on the client side, "Login failed" at the EAP-Failure: the identity, the start of the method, the server's TLS flight, the client's last response (its TLS alert, cut after the EAP type), the EAP-Failure less than 5 s after it, and the AP's deauthentication. Note Mist's auth failure: authorization_reason_code, authorization_reason_msg, the codes in its text and its delay.
eduroam-wrong-password¶
The one failed login: eduroam-wrong.conf, as eduroam-cert-refused. A second run only where the first recording is unusable, never a third.
Expected: on the client side, "Login failed": the identity, the TLS tunnel, the exchange inside it, the EAP-Failure less than 5 s after the client's last response, and the AP's deauthentication. Mist's fields as for eduroam-cert-refused. Afterwards rm ~/linkside-lab/eduroam.conf ~/linkside-lab/eduroam-wrong.conf.
Phone client, two radios¶
Both sticks in the Pi; the phone is the client, the Fedora the iperf3 server (an iperf3 app on the phone, or a large download).
sticky¶
The AP45 at high power, set on its page in Mist. The phone connects to the AP45 in the office, then goes with the sensor toward the living room, to a spot where the AP45 is at most −70 dBm at the sensor and the second AP45 at least 8 dB stronger; the roam target in New Session is the second AP45's 5 GHz radio. Five minutes there with traffic.
Expected: on the client side, "Stays on a weak AP", when the phone stays. A phone that roams there gives a completed roam instead, with Roaming Good: note at what signal of AP A it left. The phone tends to roam; this scenario is best effort until a second Linux client can be pinned with bssid=.
ping-pong¶
Both AP45s at the same power on their 5 GHz channels (Set power in the RF template), the phone halfway between them with traffic, moved a little every 15 s for three minutes; the roam target the second AP45's radio.
Expected: on the client side, "Roams back and forth" (at least three roams between the two APs within 60 s), at the clock time of the first. Fewer roams: Roaming Good with the slowest roam as its note.
weaker-roam¶
The phone carried from the AP45 toward the second AP45 and back, with the sensor carried along next to it, with traffic; the roam target the second AP45's radio.
Expected: one or two roams, Roaming Good with "up to n ms", unless the phone moves to an AP at least 6 dB weaker at the sensor: then "Moves to a weaker AP". With the sensor left behind, the client is away from it and the rule stays untested ("client away from the sensor"): the walk test of u7-phone-ft-roam.
Recorded with the sensor left behind on 2026-11-02, the phone on eduroam between the two AP45s, outside the repo (roam-ap45.md).
power-save¶
The phone on eduroam on the AP45, the sensor 1 m from it, both still, the Mac's Wi-Fi off the channel. Two sessions of a few minutes at the same spot: one with a speed test or a large download, which keeps the phone awake, and one with a stream it buffers (a video or an audio stream), which lets it doze between chunks. The screen does not decide: a YouTube video fetches a chunk about every 10 s, and with the screen on the phone dozed between them, 92 % of the downlink right after a wake (testlab-ap-kick-eduroam). Note the AP45's channel and width, which Mist may change between the two.
Expected with the stream: wakes every 1.5 to 3 s, the AP's backlog in a burst after each, and in some of those bursts many of the AP's frames retried (linkside-cli replay gives the powersave line). With the speed test: few wakes, and the share of retries as at a clean channel. When the retries come right after wakes and not during the speed test, the pause is the cause: the phone's power save, or the AP's rate after the pause. A second client tells those apart: the A9000 on the Fedora with power save on (iw dev <if> set power_save on), or a laptop on eduroam, fetching 1 MB every 10 s (curl -so /dev/null https://proof.ovh.net/files/1Mb.dat in a loop: eduroam on the AP45 leaves at the campus, so the Fedora's iperf3 servers are out of reach there, and on testwlan-1 they are not); the AP choosing the same rates in that client's bursts, with the same losses, points at the AP. With retries during the speed test as well it is not the pause.
Recorded on 2026-10-21 as testlab-power-save-video, the download outside the repo for its size, and with the MacBook as the second client as testlab-power-save-second-client (power-save.md), capB on Automatic in both.
long-quiet¶
The phone on the AP45 for 30 to 60 minutes of normal use: a call, a video, browsing, idle stretches; the roam target the second AP45's radio. Then the same on campus beside a Mist AP where nothing is wrong, with an own device other than the Mac as the client, as a case of its own.
Expected: no hint in any window; questions without traffic Untested, not Poor. Every hint found is a false alarm: note its rule, window and numbers. The campus run is the first in dense RF: many co-channel radios, a high QBSS and weak second APs, where congestion and weak-coverage would raise them.
Recorded on 2026-10-21 on the AP45 beside Mist instead of AP A, 51 minutes with capB on the AP45's 2.4 GHz radio, as testlab-long-quiet, cut to the stretches with the findings.
ip-services¶
The phone on eduroam on the AP45, as for power-save; eduroam leaves at the campus, so its DHCP and DNS are the campus's and cannot be made slow at home. Three sessions, each with a reconnect after the change in the phone's settings for eduroam, and a few web pages opened: Configure DNS set to Manual with 192.0.2.1 alone (TEST-NET-1, nothing answers); Configure IP set to Manual with the phone's address and mask and as router an address of the subnet that no host has; and Configure IP set to Manual with 198.51.100.10/24 and router 198.51.100.1 (TEST-NET-2, another subnet). All back to Automatic afterwards.
Expected: for the DNS server of its own "DNS fails", beyond the Wi-Fi, from MARVIS_DNS_FAILURE, or "Own DNS server blocked", client side, from MARVIS_EVENT_CLIENT_STATIC_DNS_BLOCKED where the WLAN blocks such servers; for the gateway "The gateway does not answer", beyond the Wi-Fi, from CLIENT_GW_ARP_FAILURE, or "Static IP address blocked", client side, from MARVIS_EVENT_CLIENT_STATIC_IP_BLOCKED where the WLAN admits only addresses from DHCP; for the address of another subnet "Invalid IP address", undecided, from MARVIS_EVENT_INVALID_CLIENT_IP, or the blocked static address, while the gateway's failure alone reads beyond the Wi-Fi, wrongly; and the timeline's DHCP, ARP and DNS lines after the association. Note which events Mist gives, with their fields, type_code and delay, and whether they come at all (the event types).
Recorded on 2026-10-23 (mist-events.md): the DNS server of its own as expected, as testlab-ip-services-dns; the gateway and the other subnet stay outside the repo: Mist gave no event for them, as it checks the gateway only after a DHCP Ack, and the app found nothing abnormal. Mobile data off first: iOS otherwise loads the pages over it, and the failure looks like none.
Later¶
Scenarios without a section of their own yet. The 802.1X ones run on FreeRADIUS, for what eduroam cannot be made to show: FreeRADIUS 3.2 in a container on the Fedora, two WLANs of the site's own beside testwlan-1, untagged on the home LAN, one with WPA2-Enterprise and one with WPA3-Enterprise (PMF required), PEAP-MSCHAPv2 and EAP-TLS with test users and a private CA, and the RADIUS latency set with tc qdisc add dev <if> root netem delay <ms> on its host.
| Scenario | How to cause it | Expected over the air | Reference client | Outcome |
|---|---|---|---|---|
| Slow RADIUS | A netem delay of 500 to 2000 ms on the RADIUS host |
Long gaps between an EAP-Response and the next Request; possibly an EAP timeout | Same delay | upstream / not Wi-Fi |
| Roaming without fast roaming | 802.1X (PEAP) without FT, a walk between the APs | Reassociation, a full EAP, the 4-way; a roam of a few hundred ms, most of it RADIUS | Same | AP side (no fast roaming) |
| Client without 802.11r | FT on, a client that does not do FT, a walk between the APs | A reassociation with a non-FT AKM and a full EAP login while the beacons offer FT | Roams with FT | client side |
| Slow RADIUS while roaming | Roaming without fast roaming plus the netem delay |
Long EAP gaps inside the roam | Same delay | upstream (RADIUS), no fast roaming found as well |
| Interference at the AP | An interferer next to the AP45, the client at a distance; an AP pcap of the scanning radio and the sensor under the AP as second vantage points | UL BA success low, DL better; Mist: a lower client SNR, util_non_wifi up |
Suffers too in the UL | AP side |
| Asymmetric link | The AP45 at full power, the phone at the edge of its coverage | The AP strong at the sensor, the client with many retries or low rates, a low Power Capability; Mist: the client's RSSI much lower | Depends on its own transmit power | AP side |
| WPA3 transition mode | testwlan-1 (WPA2-PSK and WPA3-SAE, PMF optional, 802.11r) with BTM on; a WPA2 client without PMF and a WPA3 client side by side; kick both, then steer both with BTM |
The WPA2 client: reasons and BTM readable; the WPA3 client: protected action frames, the reasons only in Mist | The WPA3 client | baseline |
| OFDMA | OFDMA on a U7, several clients with traffic; the same on the AP45 or an AP47 for all-acks | Trigger frames with the client's AID, a Multi-STA BA: from the U7 a bitmap per entry, from an AP with all-acks Ack Type 1 and TID 14 (all-ack.md); TB PPDUs not decodable | — | baseline, a check of the UL derivation |
| MLO and eMLSR reconnects | The A9000 client on a U7's MLO SSID via 6 GHz; a Wi-Fi 7 client with eMLSR | The ML element with the per-STA profiles and the EML Capabilities, then traffic on the links | — | baseline |