From: Felix Fietkau Date: Fri, 31 Jul 2026 01:19:42 +0000 (+0200) Subject: hostapd: only follow a station channel on radios that hold a link X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=aa2329f858010c1aaa7f81ccc12c048dbb397e78;p=thirdparty%2Fopenwrt.git hostapd: only follow a station channel on radios that hold a link A station MLD is configured across every radio it may use, which marks all of those radios as channel following. Only the radios the station actually holds a link on have a channel to follow; the rest waited for one that never arrived, so they ignored their configured channel and kept whatever ACS had picked at start up. Later channel changes were dropped as well, because the pending follow was treated as authoritative. Track the frequency reported through apsta_state per radio and let a radio without one apply its own configuration. The supplicant marks the radios holding no link when the station completes an association, and repeats the notification on link reconfiguration, so radios are handed back and forth as the station adds or drops links rather than staying stuck on the first association. Signed-off-by: Felix Fietkau --- diff --git a/package/network/services/hostapd/files/hostapd.uc b/package/network/services/hostapd/files/hostapd.uc index 919c05fcc2e..a7fbc6ec057 100644 --- a/package/network/services/hostapd/files/hostapd.uc +++ b/package/network/services/hostapd/files/hostapd.uc @@ -16,6 +16,7 @@ libubus.guard(ex_handler); hostapd.data.config = {}; hostapd.data.pending_config = {}; +hostapd.data.apsta_freq = {}; hostapd.data.file_fields = { vlan_file: true, @@ -617,8 +618,13 @@ function iface_channel_switch(name, config) * interface (STA/mesh/adhoc) via apsta_state; the AP never picks its * own channel here. Adopt the new fallback channel without touching the * running BSSes. + * + * A station MLD marks every radio it spans as channel following, yet it + * only drives the channel of the radios it holds a link on. Radios + * without a link have no channel to follow, so let them apply their own + * configuration instead of deferring to a station that never reports one. */ - if (radio.channel_follow) + if (radio.channel_follow && hostapd.data.apsta_freq[name]) return true; /* @@ -1001,6 +1007,7 @@ function iface_check_mld(phydev, name, config) function iface_config_remove(name, old_config) { + delete hostapd.data.apsta_freq[name]; hostapd.remove_iface(name); return iface_remove(old_config); } @@ -1436,6 +1443,7 @@ let main_obj = { sec_chan_offset: 0, csa: true, csa_count: 0, + no_link: true, }, call: function(req) { let phy = phy_name(req.args.phy, req.args.radio); @@ -1451,10 +1459,32 @@ let main_obj = { return 0; if (!req.args.up) { + delete hostapd.data.apsta_freq[phy]; iface.stop(); return 0; } + if (req.args.frequency) + hostapd.data.apsta_freq[phy] = req.args.frequency; + else if (req.args.no_link && hostapd.data.apsta_freq[phy]) { + /* + * The station gave up the link that dictated this radio's + * channel. Hand the radio back to its own configuration + * rather than leaving the AP on the channel the station + * happened to use last. + */ + delete hostapd.data.apsta_freq[phy]; + + if (iface_channel_switch(phy, config)) + return 0; + + let phydev = phy_open(config.phy, config.radio_idx); + if (phydev) + iface_restart(phydev, config, config); + + return 0; + } + let freq_info; if (req.args.frequency) { freq_info = iface_freq_info(iface, config, req.args); diff --git a/package/network/services/hostapd/files/wpa_supplicant.uc b/package/network/services/hostapd/files/wpa_supplicant.uc index 9effed7017a..212fc637ac5 100644 --- a/package/network/services/hostapd/files/wpa_supplicant.uc +++ b/package/network/services/hostapd/files/wpa_supplicant.uc @@ -906,9 +906,17 @@ function iface_hostapd_notify(ifname, iface, state) radio: i, }; - if (state == "COMPLETED") + if (state == "COMPLETED") { iface_status_fill_radio(mld, i, radio_msg, status); + // A station MLD spans every radio it was configured for, but only + // drives the channel of the radios it holds a link on. Tell the AP + // side which radios are left without one, so they can fall back to + // their own channel configuration. + if (!radio_msg.frequency) + radio_msg.no_link = true; + } + ubus.call("hostapd", "apsta_state", radio_msg); } } @@ -1048,6 +1056,18 @@ return { }, ctrl_event: function(name, iface, ev) { iface_ubus_notify(name, ev); + + // The set of links of an associated MLD can change without a state + // transition, which would leave the AP side following a link that no + // longer exists, or ignoring one that just appeared. + if (index(ev, "CTRL-EVENT-LINK-RECONFIG") != 0) + return; + + try { + iface_hostapd_notify(name, iface, "COMPLETED"); + } catch (e) { + wpas.printf(`Error handling link reconfiguration: ${e}`); + } }, state: function(ifname, iface, state) { let event_data = iface.status();