Skip to content

Home WiFi Roaming with OpenWrt (usteer, 802.11k/v/r) and the Cudy RE3000

Notes from October 2026: fixing “sticky” WiFi clients in a flat with three access points, replacing a stock TP-Link extender with an OpenWrt-flashed Cudy RE3000, and a MacBook that was slow on that AP, which we eventually traced to Apple’s AWDL. Everything here was measured on our own network.

AP Hardware Chip Firmware
Bedroom D-Link DAP-X1860 A1 MT7621 + MT7915 (WiFi 6, 2x2) OpenWrt 24.10.0
Kitchen D-Link DAP-X1860 A1 MT7621 + MT7915 OpenWrt 24.10.0
Study Cudy RE3000 v1 (replaced a TP-Link RE605X) MT7981 + MT7976 (WiFi 6, 3 RX chains, 2 streams) OpenWrt 25.12.5

All three broadcast the same SSID, are wired to a MikroTik hAP ac² router, and get their IP by DHCP with a static lease. 5 GHz runs 80 MHz on separate blocks (36–48, 52–64, 100–112); 2.4 GHz on channels 1, 6 and 11.

Problem 1: alert noise from “weak WiFi clients”

Section titled “Problem 1: alert noise from “weak WiFi clients””

A home-made monitoring script (runs every 15 min, alerts to Telegram) flagged any client at or below -70 dBm. Phones and a watch walking between rooms crossed that line every half hour, so it alerted and “resolved” constantly. Signal strength alone says nothing about whether a device is having a problem.

Fix: stop alerting on weak signal, keep the count as a metric. The script still alerts on the thing that actually hurts: a client with good signal stuck at a floor data rate (a known mt76 rate-control bug we hit before, worked around with a weekly 5 GHz radio restart).

Each AP was working alone: no 802.11v (the AP can’t ask a client to move), no 802.11r (switching APs means a full re-login, so clients wait until the signal is terrible), 802.11k only half set up. The bedroom AP also transmitted 3 dB louder than the kitchen one, so phones hearing it “fine” never looked for the closer AP.

On every OpenWrt AP:

  1. Full wpad instead of wpad-basic. The basic build has no 802.11v at all. Set bss_transition=1 with the basic build and hostapd refuses the whole config, so WiFi goes down. (We took one AP down for 4 minutes learning this.) Download the full package first, then swap, so the AP is never without hostapd:
    Terminal window
    opkg update && opkg download wpad-mbedtls
    opkg remove wpad-basic-mbedtls && opkg install /tmp/wpad-mbedtls*.ipk
  2. 802.11k + v + r on 5 GHz only:
    Terminal window
    uci set wireless.default_radio1.ieee80211k=1
    uci set wireless.default_radio1.bss_transition=1
    uci set wireless.default_radio1.ieee80211r=1
    uci set wireless.default_radio1.mobility_domain=4e6d # same on all APs
    uci set wireless.default_radio1.ft_over_ds=0
    uci set wireless.default_radio1.ft_psk_generate_local=1
  3. Nothing extra on 2.4 GHz. That’s where IoT lives. With 802.11k or 802.11r on 2.4 GHz, a Bosch dryer sent hundreds of “invalid association request” per minute, and bulbs, a printer and a vacuum fled to another AP. Old IoT WiFi chips can’t parse the extra information elements.
  4. usteer, with gentle settings that never kick anyone:
    Terminal window
    uci set usteer.@usteer[0].network=lan
    uci set usteer.@usteer[0].roam_scan_snr=-70 # ask client to scan below -70 dBm
    uci set usteer.@usteer[0].roam_trigger_snr=-75 # send 802.11v transition request below -75
    uci set usteer.@usteer[0].signal_diff_threshold=8 # only if the other AP is 8 dB better
    No min_snr, so far-away IoT devices are never disconnected. ubus call usteer remote_hosts should list the other APs.
  5. usteer manages the neighbour lists. If you also run static-neighbor-reports, disable it: its trigger re-applies the static list after every hostapd restart and fights usteer.
  6. Matched transmit power (bedroom 5 GHz 23 → 20 dBm).
  • Clients are sticky by design. macOS only looks for another AP below about -70 dBm, so a Mac at -55 on the “wrong” AP never moves, and usteer (trigger -75) won’t nudge it either. That’s fine as long as the link is good.
  • Firmware upgrades drop extra packages. A plain sysupgrade keeps /etc/config but loses wpad-mbedtls and usteer, leaving bss_transition=1 on the basic wpad, so no WiFi. Use owut upgrade: it builds an image on OpenWrt’s ASU server that includes your installed packages.
  • Any radio restart reshuffles clients, and they stay where they land.
  • Manually moving a device for testing: ubus call hostapd.phy1-ap0 bss_transition_request (polite, clients may decline) or del_client with ban_time (forces it; a dozing Android phone can go offline for minutes after this).

The RE605X uses a Broadcom WiFi chip and stock firmware, and OpenWrt doesn’t support it in any version, so it could never take part in usteer or 802.11r. It was the cause of the sticky roaming in the study.

Device Price seen Notes
Cudy RE3000 v1 (chosen) ~€40–45 Wall-plug, MT7981, 1 GbE port, OpenWrt since 23.05. Compact like the DAP-X1860.
Cudy WR3000 v1 ~€35–39 Same chip, external antennas, 4 ports. Better range if it can sit on a shelf.
D-Link DAP-X1860 ~€79 (occasional deals ~€30–40) Identical to the other two APs, older chip, 80 MHz max.
Xiaomi AX3000T ~€44–50 Same chip family, fiddlier to flash.

Check the hardware version on the label (the RE3000 also exists as v2.0, which OpenWrt doesn’t support). Ours said “EU1.0”.

Flashing the RE3000 (what actually worked)

Section titled “Flashing the RE3000 (what actually worked)”
  1. Warning: fresh out of the box, the RE3000 sits at 192.168.10.254 and runs a DHCP server on your LAN the moment you plug it into a switch. It handed out 192.168.10.x addresses to anything that asked. Flash it quickly, or connect it to a single computer first.
  2. Give a machine on the same switch a temporary address in that range (ip addr add 192.168.10.2/24 dev eth0) and open the stock web UI. It asks you to create an admin password first.
  3. Get Cudy’s signed intermediate OpenWrt image. It’s linked from Cudy’s OpenWrt Software Download page via a Google Drive folder (“Download OpenWrt Firmware to Remove Signature Check”); the file is RE3000 V1.zip → cudy_re3000-v1-sysupgrade.bin (OpenWrt 23.05-SNAPSHOT). Upload it under System → Firmware Upgrade. The stock UI accepts it because it’s signed for the board.
  4. The intermediate build comes up at 192.168.1.1 with DHCP and IPv6 router advertisements on, so it’s another rogue server on your LAN. Turn both off at once:
    Terminal window
    uci set dhcp.lan.ignore=1; uci commit dhcp; /etc/init.d/dnsmasq restart
    /etc/init.d/odhcpd stop; /etc/init.d/odhcpd disable
  5. Install official OpenWrt, handing it a settings bundle so it boots as a quiet AP straight away:
    • Make a .tar.gz containing only etc/uci-defaults/99-ap (sets network.lan.proto=dhcp, dhcp.lan.ignore=1, IPv6 RA/DHCPv6 off, hostname) and etc/dropbear/authorized_keys. Create it with tar --owner=0 --group=0, or dropbear may reject the keys.
    • Run sysupgrade -f /tmp/cfg.tgz /tmp/openwrt-...-cudy_re3000-v1-squashfs-sysupgrade.bin in the foreground. Ours, started as nohup … & over SSH, was silently killed and nothing happened.
  6. It came back with a DHCP address from the router; reserve it there. Then apply the roaming setup above. Keep the WiFi password out of anything sent to OpenWrt’s public build server, and set it over SSH afterwards.

Problem 3: MacBook slow on the RE3000 (cause found: AWDL)

Section titled “Problem 3: MacBook slow on the RE3000 (cause found: AWDL)”
Client Bedroom AP (MT7915) Study RE3000, 24.10.8 Study RE3000, 25.12.5
Windows laptop (Qualcomm FastConnect 6900) 461 632 427–622
MacBook (Apple WiFi) ~460 75–118 188–280

Internet download, single stream, Mbit/s. Line rate is ~630.

The Windows laptop gets full line rate from the RE3000. The MacBook, at -43 dBm with a 1200 Mbit/s link, doesn’t. Upload from the Mac is fine (~850 Mbit/s).

Short answer: the cause is AWDL on the Mac (Apple’s peer-to-peer WiFi behind AirDrop and Continuity) combined with the RE3000’s MT7981 chip. With AWDL switched off, the Mac gets full line rate from the RE3000. See Root cause below. The sections in between are how we got there.

  • It’s real packet loss on the downlink, not slow radio. From the server side (ss -ti on Linux), a download to the Mac via the RE3000 showed 1,900–5,500 retransmitted packets in 8 seconds, with the TCP congestion window collapsing to ~250–500 packets. Via the D-Link AP: 3 retransmissions, window ~2,900. On the LAN the short round trip hides it (460–680 Mbit/s); over the internet it halves the speed.
  • The loss comes and goes: identical settings gave 8 retransmissions in one run and 3,900 in the next. In hindsight, that’s AWDL activity varying over time.
  • The AP’s own queue showed no drops, and it was mostly empty during downloads.
  • The AP sends the Mac smaller batches of frames (A-MPDUs of mostly 2–10 MPDUs vs 11–28 for the Windows laptop). The Mac also advertises a larger minimum frame spacing (8 µs vs 2 µs) and doesn’t set the HE “A-MSDU in A-MPDU” bit.
  • Single- and multi-user beamforming off. That cut 802.11 retries once, but not the TCP loss.
  • vht_max_mpdu=7991 (matching the D-Links).
  • Capping the data rate at MCS 9 with iw dev phy1-ap0 set bitrates he-mcs-5 2:0-9. One run looked fixed (681 Mbit/s, 8 retransmissions); the repeat didn’t.
  • Moving the primary channel from 44 to 36 (worse).
  • Battery power saving (the Mac was on AC), the AP’s CPU (75% idle), WiFi offload (WED off), upgrading 24.10.8 → 25.12.5 (it helped ~2x but didn’t fix it).
  • Restricting the 5 GHz radio to 2 chains: the RE3000 reports Configured Antennas: TX 0x1c RX 0x1c (3 chains). iw phy phy1 set antenna 3 3 returns success but changes nothing, and the uci txantenna/rxantenna options are ignored too (openwrt#19323).

AWDL (Apple Wireless Direct Link, which powers AirDrop, Continuity, Sidecar and Universal Control) is the trigger. With sudo ifconfig awdl0 down and a fresh WiFi connection, the same Mac on the same RE3000 runs at full speed. Turning it back on brings the loss straight back:

Mac (fresh association each time) Internet LAN TCP retransmits (8 s)
RE3000, channel 44, AWDL off 611–621 880 0
RE3000, channel 44, AWDL on 222–286 587 5,400–6,800
RE3000, channel 52, AWDL on 84–101 308 ~2,500
D-Link (MT7915), channel 100, AWDL on 406–460 — ~0
  • It’s not the channel. AWDL’s European channel is 44, but moving the RE3000 to 52 made it worse. The trigger is AWDL combined with the MT7981 (the MT7915-based D-Links are unaffected).
  • Most likely mechanism (not proven): the Mac briefly leaves for AWDL and signals power-save, and the MT7981 firmware doesn’t hold frames for it, so they’re lost. The mac80211 power-save counters read 0 on both MediaTek APs (firmware handles power-save), so they can’t confirm it.
  • A connection made while AWDL was on stayed stuck at a 20 MHz downlink even after AWDL was switched off, until the Mac reconnected.
  • macOS re-enables AWDL on sleep/wake, a WiFi toggle, or when AirDrop/Handoff needs it, so awdl0 down is a test, not a fix. A permanent version needs a root LaunchDaemon that keeps it down, at the cost of AirDrop/Continuity.

We swept the study AP across every usable channel/width with AWDL on, reconnecting the Mac each time:

Study AP setting Internet (3 runs) LAN TCP retrans (LAN, ~8 s)
ch 44, 80 MHz (current) 112–330 (earlier 222–286) 298–587 375–6,800
ch 44, 40 MHz 234–341 421 1,152
ch 132, 40 MHz 160–330 294 3,308
ch 149, 80 MHz (13 dBm) 42–253 — —
ch 44, 20 MHz 96–165 205 1,579
ch 48, 80 MHz 88–125 292 1,728
ch 100, 80 MHz (shared with bedroom AP) 105–115 — —
ch 52, 80 MHz 84–101 308 ~2,500
ch 36, 80 MHz 54–113 365 4,686

Mbit/s. Line rate ~630. “—” = LAN probe failed that run.

  • No channel fixes it. Channel 44 (AWDL’s European channel) is the best block; channels further away (36, 48, 52, 100) were worse, not better.
  • 40 MHz on channel 44 was the most consistent for the Mac, but it would cap other clients (the Windows laptop drops from ~620 to roughly 450). We kept 44 at 80 MHz.
  • Results swing a lot between runs (e.g. 42 → 253 on one channel), so treat single numbers with care.

To check whether it’s Apple-only, we tested a Pixel 6 (Broadcom WiFi) with a browser-based download from a LAN server, sampling the AP’s transmit rate every second:

Study RE3000 Bedroom D-Link
Pixel 6 download 228–232 Mbit/s 166 Mbit/s
AP → phone link during the test 20 MHz, HE-MCS 10–11 20 MHz, HE-MCS 9–10

The phone limits its own width to 20 MHz on both APs, so it isn’t a useful comparison (the RE3000 even beat the D-Link). The MacBook, sampled the same way, stayed at 80 MHz MCS 11 throughout its 218 Mbit/s download, so its limit is the packet loss, not the width. Lesson: sample the AP’s tx bitrate during the transfer. Idle values say nothing.

  • What we do now: the Mac uses the D-Link AP (~460 Mbit/s, AirDrop/Continuity keep working). The Windows laptop uses the RE3000 at line rate.
  • Full speed on the RE3000: sudo ifconfig awdl0 down, then reconnect WiFi. It lasts until sleep/wake, a WiFi toggle, or the next AirDrop/Handoff use. A root LaunchDaemon can keep it down permanently, but AirDrop, Sidecar and Universal Control stop working.
  • Hardware: an MT7915-based AP (like the DAP-X1860) doesn’t show the problem. Other MT7981/MT7986 (Filogic) APs probably do; the Cudy WR3000 uses the same chip.
  • Upstream: worth reporting to mt76 with the AWDL on/off repro. No public report we found had pinned this on AWDL.
  • Newer MT7981 WiFi firmware may change things (25.12.5 still ships the 2024-08-23 build).
  • Measure throughput, not dBm. A client at -55 dBm can be fine; a client at -62 dBm can be stuck at 8 Mbit/s.
  • One client being fast proves nothing about another client on the same AP.
  • Slow Mac on an otherwise fine AP? Test with AWDL off first (sudo ifconfig awdl0 down, then reconnect). It took us a day of driver tuning to get there.
  • Reconnect the client after every change. A connection set up under the old conditions can stay degraded (here: stuck at 20 MHz).
  • Measure loss from the sender’s side (ss -ti on a Linux server). The AP’s tx failed counter on MT7981 was not trustworthy: it matched retries 1:1 and kept climbing even on clean runs.
  • pkill -f "<pattern>" inside an ssh host '…' command can match and kill that very SSH session. Use pkill -f "[p]attern".
  • On macOS, tools installed outside the system (e.g. a mise/Homebrew Python) can be blocked from the LAN by Local Network privacy. The symptom is No route to host while ping works.