ieee80211_is_data(hdr->frame_control);
 }
 
+/**
+ * ieee80211_set_active_links - set active links in client mode
+ * @vif: interface to set active links on
+ * @active_links: the new active links bitmap
+ *
+ * This changes the active links on an interface. The interface
+ * must be in client mode (in AP mode, all links are always active),
+ * and @active_links must be a subset of the vif's valid_links.
+ *
+ * If a link is switched off and another is switched on at the same
+ * time (e.g. active_links going from 0x1 to 0x10) then you will get
+ * a sequence of calls like
+ *  - change_vif_links(0x11)
+ *  - unassign_vif_chanctx(link_id=0)
+ *  - change_sta_links(0x11) for each affected STA (the AP)
+ *    (TDLS connections on now inactive links should be torn down)
+ *  - remove group keys on the old link (link_id 0)
+ *  - add new group keys (GTK/IGTK/BIGTK) on the new link (link_id 4)
+ *  - change_sta_links(0x10) for each affected STA (the AP)
+ *  - assign_vif_chanctx(link_id=4)
+ *  - change_vif_links(0x10)
+ *
+ * Note: This function acquires some mac80211 locks and must not
+ *      be called with any driver locks held that could cause a
+ *      lock dependency inversion. Best call it without locks.
+ */
+int ieee80211_set_active_links(struct ieee80211_vif *vif, u16 active_links);
+
+/**
+ * ieee80211_set_active_links_async - asynchronously set active links
+ * @vif: interface to set active links on
+ * @active_links: the new active links bitmap
+ *
+ * See ieee80211_set_active_links() for more information, the only
+ * difference here is that the link change is triggered async and
+ * can be called in any context, but the link switch will only be
+ * completed after it returns.
+ */
+void ieee80211_set_active_links_async(struct ieee80211_vif *vif,
+                                     u16 active_links);
+
 #endif /* MAC80211_H */
 
 }
 IEEE80211_IF_FILE_RW(tsf);
 
+static ssize_t ieee80211_if_fmt_valid_links(const struct ieee80211_sub_if_data *sdata,
+                                           char *buf, int buflen)
+{
+       return snprintf(buf, buflen, "0x%x\n", sdata->vif.valid_links);
+}
+IEEE80211_IF_FILE_R(valid_links);
+
+static ssize_t ieee80211_if_fmt_active_links(const struct ieee80211_sub_if_data *sdata,
+                                            char *buf, int buflen)
+{
+       return snprintf(buf, buflen, "0x%x\n", sdata->vif.active_links);
+}
+
+static ssize_t ieee80211_if_parse_active_links(struct ieee80211_sub_if_data *sdata,
+                                              const char *buf, int buflen)
+{
+       u16 active_links;
+
+       if (kstrtou16(buf, 0, &active_links))
+               return -EINVAL;
+
+       return ieee80211_set_active_links(&sdata->vif, active_links) ?: buflen;
+}
+IEEE80211_IF_FILE_RW(active_links);
 
 #ifdef CONFIG_MAC80211_MESH
 IEEE80211_IF_FILE(estab_plinks, u.mesh.estab_plinks, ATOMIC);
        DEBUGFS_ADD_MODE(uapsd_queues, 0600);
        DEBUGFS_ADD_MODE(uapsd_max_sp_len, 0600);
        DEBUGFS_ADD_MODE(tdls_wider_bw, 0600);
+       DEBUGFS_ADD_MODE(valid_links, 0200);
+       DEBUGFS_ADD_MODE(active_links, 0600);
 }
 
 static void add_ap_files(struct ieee80211_sub_if_data *sdata)
 
        struct ieee80211_link_data deflink;
        struct ieee80211_link_data __rcu *link[IEEE80211_MLD_MAX_NUM_LINKS];
 
+       /* for ieee80211_set_active_links_async() */
+       struct work_struct activate_links_work;
+       u16 desired_active_links;
+
 #ifdef CONFIG_MAC80211_DEBUGFS
        struct {
                struct dentry *subdir_stations;
 
                ieee80211_stop_mbssid(sdata);
        }
 
+       cancel_work_sync(&sdata->activate_links_work);
+
        wiphy_lock(sdata->local->hw.wiphy);
        ieee80211_do_stop(sdata, true);
        wiphy_unlock(sdata->local->hw.wiphy);
        ieee80211_recalc_smps(sdata, &sdata->deflink);
 }
 
+static void ieee80211_activate_links_work(struct work_struct *work)
+{
+       struct ieee80211_sub_if_data *sdata =
+               container_of(work, struct ieee80211_sub_if_data,
+                            activate_links_work);
+
+       ieee80211_set_active_links(&sdata->vif, sdata->desired_active_links);
+}
+
 /*
  * Helper function to initialise an interface to a specific type.
  */
        skb_queue_head_init(&sdata->status_queue);
        INIT_WORK(&sdata->work, ieee80211_iface_work);
        INIT_WORK(&sdata->recalc_smps, ieee80211_recalc_smps_work);
+       INIT_WORK(&sdata->activate_links_work, ieee80211_activate_links_work);
 
        switch (type) {
        case NL80211_IFTYPE_P2P_GO:
 
        }
 }
 EXPORT_SYMBOL_GPL(ieee80211_key_replay);
+
+int ieee80211_key_switch_links(struct ieee80211_sub_if_data *sdata,
+                              unsigned long del_links_mask,
+                              unsigned long add_links_mask)
+{
+       struct ieee80211_key *key;
+       int ret;
+
+       list_for_each_entry(key, &sdata->key_list, list) {
+               if (key->conf.link_id < 0 ||
+                   !(del_links_mask & BIT(key->conf.link_id)))
+                       continue;
+
+               /* shouldn't happen for per-link keys */
+               WARN_ON(key->sta);
+
+               ieee80211_key_disable_hw_accel(key);
+       }
+
+       list_for_each_entry(key, &sdata->key_list, list) {
+               if (key->conf.link_id < 0 ||
+                   !(add_links_mask & BIT(key->conf.link_id)))
+                       continue;
+
+               /* shouldn't happen for per-link keys */
+               WARN_ON(key->sta);
+
+               ret = ieee80211_key_enable_hw_accel(key);
+               if (ret)
+                       return ret;
+       }
+
+       return 0;
+}
 
 void ieee80211_free_sta_keys(struct ieee80211_local *local,
                             struct sta_info *sta);
 void ieee80211_reenable_keys(struct ieee80211_sub_if_data *sdata);
+int ieee80211_key_switch_links(struct ieee80211_sub_if_data *sdata,
+                              unsigned long del_links_mask,
+                              unsigned long add_links_mask);
 
 #define key_mtx_dereference(local, ref) \
        rcu_dereference_protected(ref, lockdep_is_held(&((local)->key_mtx)))
 
 #include <net/mac80211.h>
 #include "ieee80211_i.h"
 #include "driver-ops.h"
+#include "key.h"
 
 void ieee80211_link_setup(struct ieee80211_link_data *link)
 {
 
        ieee80211_free_links(sdata, links);
 }
+
+static int _ieee80211_set_active_links(struct ieee80211_sub_if_data *sdata,
+                                      u16 active_links)
+{
+       struct ieee80211_bss_conf *link_confs[IEEE80211_MLD_MAX_NUM_LINKS];
+       struct ieee80211_local *local = sdata->local;
+       u16 old_active = sdata->vif.active_links;
+       unsigned long rem = old_active & ~active_links;
+       unsigned long add = active_links & ~old_active;
+       struct sta_info *sta;
+       unsigned int link_id;
+       int ret, i;
+
+       if (!ieee80211_sdata_running(sdata))
+               return -ENETDOWN;
+
+       if (sdata->vif.type != NL80211_IFTYPE_STATION)
+               return -EINVAL;
+
+       /* cannot activate links that don't exist */
+       if (active_links & ~sdata->vif.valid_links)
+               return -EINVAL;
+
+       /* nothing to do */
+       if (old_active == active_links)
+               return 0;
+
+       for (i = 0; i < IEEE80211_MLD_MAX_NUM_LINKS; i++)
+               link_confs[i] = sdata_dereference(sdata->vif.link_conf[i],
+                                                 sdata);
+
+       if (add) {
+               sdata->vif.active_links |= active_links;
+               ret = drv_change_vif_links(local, sdata,
+                                          old_active,
+                                          sdata->vif.active_links,
+                                          link_confs);
+               if (ret) {
+                       sdata->vif.active_links = old_active;
+                       return ret;
+               }
+       }
+
+       for_each_set_bit(link_id, &rem, IEEE80211_MLD_MAX_NUM_LINKS) {
+               struct ieee80211_link_data *link;
+
+               link = sdata_dereference(sdata->link[link_id], sdata);
+
+               /* FIXME: kill TDLS connections on the link */
+
+               ieee80211_link_release_channel(link);
+       }
+
+       list_for_each_entry(sta, &local->sta_list, list) {
+               if (sdata != sta->sdata)
+                       continue;
+               ret = drv_change_sta_links(local, sdata, &sta->sta,
+                                          old_active,
+                                          old_active | active_links);
+               WARN_ON_ONCE(ret);
+       }
+
+       ret = ieee80211_key_switch_links(sdata, rem, add);
+       WARN_ON_ONCE(ret);
+
+       list_for_each_entry(sta, &local->sta_list, list) {
+               if (sdata != sta->sdata)
+                       continue;
+               ret = drv_change_sta_links(local, sdata, &sta->sta,
+                                          old_active | active_links,
+                                          active_links);
+               WARN_ON_ONCE(ret);
+       }
+
+       for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+               struct ieee80211_link_data *link;
+
+               link = sdata_dereference(sdata->link[link_id], sdata);
+
+               ret = ieee80211_link_use_channel(link, &link->conf->chandef,
+                                                IEEE80211_CHANCTX_SHARED);
+               WARN_ON_ONCE(ret);
+
+               ieee80211_link_info_change_notify(sdata, link,
+                                                 BSS_CHANGED_ERP_CTS_PROT |
+                                                 BSS_CHANGED_ERP_PREAMBLE |
+                                                 BSS_CHANGED_ERP_SLOT |
+                                                 BSS_CHANGED_HT |
+                                                 BSS_CHANGED_BASIC_RATES |
+                                                 BSS_CHANGED_BSSID |
+                                                 BSS_CHANGED_CQM |
+                                                 BSS_CHANGED_QOS |
+                                                 BSS_CHANGED_TXPOWER |
+                                                 BSS_CHANGED_BANDWIDTH |
+                                                 BSS_CHANGED_TWT |
+                                                 BSS_CHANGED_HE_OBSS_PD |
+                                                 BSS_CHANGED_HE_BSS_COLOR);
+               ieee80211_mgd_set_link_qos_params(link);
+       }
+
+       old_active = sdata->vif.active_links;
+       sdata->vif.active_links = active_links;
+
+       if (rem) {
+               ret = drv_change_vif_links(local, sdata, old_active,
+                                          active_links, link_confs);
+               WARN_ON_ONCE(ret);
+       }
+
+       return 0;
+}
+
+int ieee80211_set_active_links(struct ieee80211_vif *vif, u16 active_links)
+{
+       struct ieee80211_sub_if_data *sdata = vif_to_sdata(vif);
+       struct ieee80211_local *local = sdata->local;
+       u16 old_active;
+       int ret;
+
+       sdata_lock(sdata);
+       mutex_lock(&local->sta_mtx);
+       mutex_lock(&local->mtx);
+       mutex_lock(&local->key_mtx);
+       old_active = sdata->vif.active_links;
+       if (old_active & active_links) {
+               /*
+                * if there's at least one link that stays active across
+                * the change then switch to it (to those) first, and
+                * then enable the additional links
+                */
+               ret = _ieee80211_set_active_links(sdata,
+                                                 old_active & active_links);
+               if (!ret)
+                       ret = _ieee80211_set_active_links(sdata, active_links);
+       } else {
+               /* otherwise switch directly */
+               ret = _ieee80211_set_active_links(sdata, active_links);
+       }
+       mutex_unlock(&local->key_mtx);
+       mutex_unlock(&local->mtx);
+       mutex_unlock(&local->sta_mtx);
+       sdata_unlock(sdata);
+
+       return ret;
+}
+EXPORT_SYMBOL_GPL(ieee80211_set_active_links);
+
+void ieee80211_set_active_links_async(struct ieee80211_vif *vif,
+                                     u16 active_links)
+{
+       struct ieee80211_sub_if_data *sdata = vif_to_sdata(vif);
+
+       if (!ieee80211_sdata_running(sdata))
+               return;
+
+       if (sdata->vif.type != NL80211_IFTYPE_STATION)
+               return;
+
+       /* cannot activate links that don't exist */
+       if (active_links & ~sdata->vif.valid_links)
+               return;
+
+       /* nothing to do */
+       if (sdata->vif.active_links == active_links)
+               return;
+
+       sdata->desired_active_links = active_links;
+       schedule_work(&sdata->activate_links_work);
+}
+EXPORT_SYMBOL_GPL(ieee80211_set_active_links_async);