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();