mac80211: select the link a management frame asks for

ieee80211_mgmt_tx() walks the links of an AP MLD to decide whether a
frame needs an off-channel transmission. It pins the link that holds the
requested channel only when the destination is a station that already
associated as an MLD. A frame to an address the access point does not
know keeps link_id at -1, the link reaches the driver unspecified, and
the driver picks the default link, which holds a different channel and a
different address.

A DPP Authentication Request shows this: the enrollee announces itself
on channel 6, the access point answers under the address of its 5 GHz
link, and the enrollee never sees the frame.

Pin the link whenever an active link holds the requested channel.
ieee80211_tx_skb_tid() warns on a link id for an interface that is not
an MLD and on one that names an inactive link, and active_links is zero
for an interface that is not an MLD, so that one test covers both. A
station MLD that sends from its MLD address to an address other than its
AP gets the same pin.

A link whose own address is the MLD address also matched the frame on
that address and ended the search, so a lower link that uses the MLD
address hid the link with the requested channel. Skip such a link when
the frame names another channel.

Frames to a station keep their present behaviour: the branch runs only
where the caller named a channel, and where the destination is a station
that associated as an MLD the same branch already pinned the link.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
Felix Fietkau committed 2026-10-08 09:50:37 +02:00
1 parent 1e5d762595
commit 595166fcfc
1 file changed
+69
@@ -0,0 +1,69 @@
From: Felix Fietkau <nbd@nbd.name>
Date: Tue, 08 Sep 2026 14:00:00 +0000
Subject: [PATCH] wifi: mac80211: select the link a management frame asks for
ieee80211_mgmt_tx() walks the links of an AP MLD to decide whether the
frame needs an off-channel transmission. It pins the link to the one
that holds the requested channel only when the destination is a station
that is already associated as an MLD. A frame to an address the access
point does not know keeps link_id at -1.
ieee80211_tx_skb_tid_band() then sees a frame whose transmitter address
is the MLD address and marks the link as IEEE80211_TX_CTRL_MLO_LINK
unspecified. The driver picks the default link, which is not the link
that holds the requested channel. mt7996 rewrites addr2 with the address
of that link and hands the frame to its phy, so the frame leaves on the
wrong band under the wrong address.
A DPP Authentication Request shows this. The enrollee announces itself
on channel 6, the access point answers on the 5 GHz link, and the
enrollee never sees the frame.
The channel the caller names identifies the link on its own. Pin the
link whenever the frame comes from the MLD address and an active link
holds that channel, whether or not the destination is a known station.
ieee80211_tx_skb_tid() warns on a link id for an interface that is not
an MLD and on one that names an inactive link, and active_links is zero
for an interface that is not an MLD, so that one test covers both. The
same holds for a station MLD that sends from its MLD address to an
address other than its AP.
A link whose own address is the MLD address matched the frame on that
address first and ended the search, so the link with the requested
channel was never reached when a lower link uses the MLD address. Skip
such a link when the frame names another channel.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
--- a/net/mac80211/offchannel.c
+++ b/net/mac80211/offchannel.c
@@ -926,6 +926,7 @@ int ieee80211_mgmt_tx(struct wiphy *wiph
/* Check all the links first */
for (i = 0; i < ARRAY_SIZE(sdata->vif.link_conf); i++) {
struct ieee80211_bss_conf *conf;
+ bool pin;
conf = rcu_dereference(sdata->vif.link_conf[i]);
if (!conf)
@@ -935,12 +936,18 @@ int ieee80211_mgmt_tx(struct wiphy *wiph
if (!chanctx_conf)
continue;
- if (mlo_sta && params->chan == chanctx_conf->def.chan &&
- ether_addr_equal(sdata->vif.addr, mgmt->sa)) {
+ pin = (mlo_sta || (sdata->vif.active_links & BIT(i))) &&
+ ether_addr_equal(sdata->vif.addr, mgmt->sa);
+ if (pin && params->chan == chanctx_conf->def.chan) {
link_id = i;
break;
}
+ if (pin && params->chan) {
+ chanctx_conf = NULL;
+ continue;
+ }
+
if (ether_addr_equal(conf->addr, mgmt->sa)) {
/* If userspace requested Tx on a specific link
* use the same link id if the link bss is matching