Merge git://git.kvack.org/~bcrl/aio-next
[cascardo/linux.git] / net / mac80211 / tdls.c
index 4ea25de..55ddd77 100644 (file)
@@ -35,19 +35,101 @@ void ieee80211_tdls_peer_del_work(struct work_struct *wk)
        mutex_unlock(&local->mtx);
 }
 
-static void ieee80211_tdls_add_ext_capab(struct sk_buff *skb)
+static void ieee80211_tdls_add_ext_capab(struct ieee80211_local *local,
+                                        struct sk_buff *skb)
 {
        u8 *pos = (void *)skb_put(skb, 7);
+       bool chan_switch = local->hw.wiphy->features &
+                          NL80211_FEATURE_TDLS_CHANNEL_SWITCH;
 
        *pos++ = WLAN_EID_EXT_CAPABILITY;
        *pos++ = 5; /* len */
        *pos++ = 0x0;
        *pos++ = 0x0;
        *pos++ = 0x0;
-       *pos++ = 0x0;
+       *pos++ = chan_switch ? WLAN_EXT_CAPA4_TDLS_CHAN_SWITCH : 0;
        *pos++ = WLAN_EXT_CAPA5_TDLS_ENABLED;
 }
 
+static u8
+ieee80211_tdls_add_subband(struct ieee80211_sub_if_data *sdata,
+                          struct sk_buff *skb, u16 start, u16 end,
+                          u16 spacing)
+{
+       u8 subband_cnt = 0, ch_cnt = 0;
+       struct ieee80211_channel *ch;
+       struct cfg80211_chan_def chandef;
+       int i, subband_start;
+
+       for (i = start; i <= end; i += spacing) {
+               if (!ch_cnt)
+                       subband_start = i;
+
+               ch = ieee80211_get_channel(sdata->local->hw.wiphy, i);
+               if (ch) {
+                       /* we will be active on the channel */
+                       u32 flags = IEEE80211_CHAN_DISABLED |
+                                   IEEE80211_CHAN_NO_IR;
+                       cfg80211_chandef_create(&chandef, ch,
+                                               NL80211_CHAN_HT20);
+                       if (cfg80211_chandef_usable(sdata->local->hw.wiphy,
+                                                   &chandef, flags)) {
+                               ch_cnt++;
+                               continue;
+                       }
+               }
+
+               if (ch_cnt) {
+                       u8 *pos = skb_put(skb, 2);
+                       *pos++ = ieee80211_frequency_to_channel(subband_start);
+                       *pos++ = ch_cnt;
+
+                       subband_cnt++;
+                       ch_cnt = 0;
+               }
+       }
+
+       return subband_cnt;
+}
+
+static void
+ieee80211_tdls_add_supp_channels(struct ieee80211_sub_if_data *sdata,
+                                struct sk_buff *skb)
+{
+       /*
+        * Add possible channels for TDLS. These are channels that are allowed
+        * to be active.
+        */
+       u8 subband_cnt;
+       u8 *pos = skb_put(skb, 2);
+
+       *pos++ = WLAN_EID_SUPPORTED_CHANNELS;
+
+       /*
+        * 5GHz and 2GHz channels numbers can overlap. Ignore this for now, as
+        * this doesn't happen in real world scenarios.
+        */
+
+       /* 2GHz, with 5MHz spacing */
+       subband_cnt = ieee80211_tdls_add_subband(sdata, skb, 2412, 2472, 5);
+
+       /* 5GHz, with 20MHz spacing */
+       subband_cnt += ieee80211_tdls_add_subband(sdata, skb, 5000, 5825, 20);
+
+       /* length */
+       *pos = 2 * subband_cnt;
+}
+
+static void ieee80211_tdls_add_bss_coex_ie(struct sk_buff *skb)
+{
+       u8 *pos = (void *)skb_put(skb, 3);
+
+       *pos++ = WLAN_EID_BSS_COEX_2040;
+       *pos++ = 1; /* len */
+
+       *pos++ = WLAN_BSS_COEX_INFORMATION_REQUEST;
+}
+
 static u16 ieee80211_get_tdls_sta_capab(struct ieee80211_sub_if_data *sdata,
                                        u16 status_code)
 {
@@ -190,6 +272,7 @@ ieee80211_tdls_add_setup_start_ies(struct ieee80211_sub_if_data *sdata,
 
        ieee80211_add_srates_ie(sdata, skb, false, band);
        ieee80211_add_ext_srates_ie(sdata, skb, false, band);
+       ieee80211_tdls_add_supp_channels(sdata, skb);
 
        /* add any custom IEs that go before Extended Capabilities */
        if (extra_ies_len) {
@@ -209,7 +292,7 @@ ieee80211_tdls_add_setup_start_ies(struct ieee80211_sub_if_data *sdata,
                offset = noffset;
        }
 
-       ieee80211_tdls_add_ext_capab(skb);
+       ieee80211_tdls_add_ext_capab(local, skb);
 
        /* add the QoS element if we support it */
        if (local->hw.queues >= IEEE80211_NUM_ACS &&
@@ -271,6 +354,10 @@ ieee80211_tdls_add_setup_start_ies(struct ieee80211_sub_if_data *sdata,
 
        rcu_read_unlock();
 
+       if (ht_cap.ht_supported &&
+           (ht_cap.cap & IEEE80211_HT_CAP_SUP_WIDTH_20_40))
+               ieee80211_tdls_add_bss_coex_ie(skb);
+
        /* add any remaining IEs */
        if (extra_ies_len) {
                noffset = extra_ies_len;
@@ -362,11 +449,68 @@ ieee80211_tdls_add_setup_cfm_ies(struct ieee80211_sub_if_data *sdata,
        ieee80211_tdls_add_link_ie(sdata, skb, peer, initiator);
 }
 
+static void
+ieee80211_tdls_add_chan_switch_req_ies(struct ieee80211_sub_if_data *sdata,
+                                      struct sk_buff *skb, const u8 *peer,
+                                      bool initiator, const u8 *extra_ies,
+                                      size_t extra_ies_len, u8 oper_class,
+                                      struct cfg80211_chan_def *chandef)
+{
+       struct ieee80211_tdls_data *tf;
+       size_t offset = 0, noffset;
+       u8 *pos;
+
+       if (WARN_ON_ONCE(!chandef))
+               return;
+
+       tf = (void *)skb->data;
+       tf->u.chan_switch_req.target_channel =
+               ieee80211_frequency_to_channel(chandef->chan->center_freq);
+       tf->u.chan_switch_req.oper_class = oper_class;
+
+       if (extra_ies_len) {
+               static const u8 before_lnkie[] = {
+                       WLAN_EID_SECONDARY_CHANNEL_OFFSET,
+               };
+               noffset = ieee80211_ie_split(extra_ies, extra_ies_len,
+                                            before_lnkie,
+                                            ARRAY_SIZE(before_lnkie),
+                                            offset);
+               pos = skb_put(skb, noffset - offset);
+               memcpy(pos, extra_ies + offset, noffset - offset);
+               offset = noffset;
+       }
+
+       ieee80211_tdls_add_link_ie(sdata, skb, peer, initiator);
+
+       /* add any remaining IEs */
+       if (extra_ies_len) {
+               noffset = extra_ies_len;
+               pos = skb_put(skb, noffset - offset);
+               memcpy(pos, extra_ies + offset, noffset - offset);
+       }
+}
+
+static void
+ieee80211_tdls_add_chan_switch_resp_ies(struct ieee80211_sub_if_data *sdata,
+                                       struct sk_buff *skb, const u8 *peer,
+                                       u16 status_code, bool initiator,
+                                       const u8 *extra_ies,
+                                       size_t extra_ies_len)
+{
+       if (status_code == 0)
+               ieee80211_tdls_add_link_ie(sdata, skb, peer, initiator);
+
+       if (extra_ies_len)
+               memcpy(skb_put(skb, extra_ies_len), extra_ies, extra_ies_len);
+}
+
 static void ieee80211_tdls_add_ies(struct ieee80211_sub_if_data *sdata,
                                   struct sk_buff *skb, const u8 *peer,
                                   u8 action_code, u16 status_code,
                                   bool initiator, const u8 *extra_ies,
-                                  size_t extra_ies_len)
+                                  size_t extra_ies_len, u8 oper_class,
+                                  struct cfg80211_chan_def *chandef)
 {
        switch (action_code) {
        case WLAN_TDLS_SETUP_REQUEST:
@@ -393,6 +537,18 @@ static void ieee80211_tdls_add_ies(struct ieee80211_sub_if_data *sdata,
                if (status_code == 0 || action_code == WLAN_TDLS_TEARDOWN)
                        ieee80211_tdls_add_link_ie(sdata, skb, peer, initiator);
                break;
+       case WLAN_TDLS_CHANNEL_SWITCH_REQUEST:
+               ieee80211_tdls_add_chan_switch_req_ies(sdata, skb, peer,
+                                                      initiator, extra_ies,
+                                                      extra_ies_len,
+                                                      oper_class, chandef);
+               break;
+       case WLAN_TDLS_CHANNEL_SWITCH_RESPONSE:
+               ieee80211_tdls_add_chan_switch_resp_ies(sdata, skb, peer,
+                                                       status_code,
+                                                       initiator, extra_ies,
+                                                       extra_ies_len);
+               break;
        }
 
 }
@@ -459,6 +615,19 @@ ieee80211_prep_tdls_encap_data(struct wiphy *wiphy, struct net_device *dev,
                skb_put(skb, sizeof(tf->u.discover_req));
                tf->u.discover_req.dialog_token = dialog_token;
                break;
+       case WLAN_TDLS_CHANNEL_SWITCH_REQUEST:
+               tf->category = WLAN_CATEGORY_TDLS;
+               tf->action_code = WLAN_TDLS_CHANNEL_SWITCH_REQUEST;
+
+               skb_put(skb, sizeof(tf->u.chan_switch_req));
+               break;
+       case WLAN_TDLS_CHANNEL_SWITCH_RESPONSE:
+               tf->category = WLAN_CATEGORY_TDLS;
+               tf->action_code = WLAN_TDLS_CHANNEL_SWITCH_RESPONSE;
+
+               skb_put(skb, sizeof(tf->u.chan_switch_resp));
+               tf->u.chan_switch_resp.status_code = cpu_to_le16(status_code);
+               break;
        default:
                return -EINVAL;
        }
@@ -502,32 +671,33 @@ ieee80211_prep_tdls_direct(struct wiphy *wiphy, struct net_device *dev,
        return 0;
 }
 
-static int
-ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
-                               const u8 *peer, u8 action_code,
-                               u8 dialog_token, u16 status_code,
-                               u32 peer_capability, bool initiator,
-                               const u8 *extra_ies, size_t extra_ies_len)
+static struct sk_buff *
+ieee80211_tdls_build_mgmt_packet_data(struct ieee80211_sub_if_data *sdata,
+                                     const u8 *peer, u8 action_code,
+                                     u8 dialog_token, u16 status_code,
+                                     bool initiator, const u8 *extra_ies,
+                                     size_t extra_ies_len, u8 oper_class,
+                                     struct cfg80211_chan_def *chandef)
 {
-       struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
        struct ieee80211_local *local = sdata->local;
-       struct sk_buff *skb = NULL;
-       bool send_direct;
-       struct sta_info *sta;
+       struct sk_buff *skb;
        int ret;
 
-       skb = dev_alloc_skb(local->hw.extra_tx_headroom +
-                           max(sizeof(struct ieee80211_mgmt),
-                               sizeof(struct ieee80211_tdls_data)) +
-                           50 + /* supported rates */
-                           7 + /* ext capab */
-                           26 + /* max(WMM-info, WMM-param) */
-                           2 + max(sizeof(struct ieee80211_ht_cap),
-                                   sizeof(struct ieee80211_ht_operation)) +
-                           extra_ies_len +
-                           sizeof(struct ieee80211_tdls_lnkie));
+       skb = netdev_alloc_skb(sdata->dev,
+                              local->hw.extra_tx_headroom +
+                              max(sizeof(struct ieee80211_mgmt),
+                                  sizeof(struct ieee80211_tdls_data)) +
+                              50 + /* supported rates */
+                              7 + /* ext capab */
+                              26 + /* max(WMM-info, WMM-param) */
+                              2 + max(sizeof(struct ieee80211_ht_cap),
+                                      sizeof(struct ieee80211_ht_operation)) +
+                              50 + /* supported channels */
+                              3 + /* 40/20 BSS coex */
+                              extra_ies_len +
+                              sizeof(struct ieee80211_tdls_lnkie));
        if (!skb)
-               return -ENOMEM;
+               return NULL;
 
        skb_reserve(skb, local->hw.extra_tx_headroom);
 
@@ -537,16 +707,18 @@ ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
        case WLAN_TDLS_SETUP_CONFIRM:
        case WLAN_TDLS_TEARDOWN:
        case WLAN_TDLS_DISCOVERY_REQUEST:
-               ret = ieee80211_prep_tdls_encap_data(wiphy, dev, peer,
+       case WLAN_TDLS_CHANNEL_SWITCH_REQUEST:
+       case WLAN_TDLS_CHANNEL_SWITCH_RESPONSE:
+               ret = ieee80211_prep_tdls_encap_data(local->hw.wiphy,
+                                                    sdata->dev, peer,
                                                     action_code, dialog_token,
                                                     status_code, skb);
-               send_direct = false;
                break;
        case WLAN_PUB_ACTION_TDLS_DISCOVER_RES:
-               ret = ieee80211_prep_tdls_direct(wiphy, dev, peer, action_code,
+               ret = ieee80211_prep_tdls_direct(local->hw.wiphy, sdata->dev,
+                                                peer, action_code,
                                                 dialog_token, status_code,
                                                 skb);
-               send_direct = true;
                break;
        default:
                ret = -ENOTSUPP;
@@ -556,14 +728,40 @@ ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
        if (ret < 0)
                goto fail;
 
+       ieee80211_tdls_add_ies(sdata, skb, peer, action_code, status_code,
+                              initiator, extra_ies, extra_ies_len, oper_class,
+                              chandef);
+       return skb;
+
+fail:
+       dev_kfree_skb(skb);
+       return NULL;
+}
+
+static int
+ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
+                               const u8 *peer, u8 action_code, u8 dialog_token,
+                               u16 status_code, u32 peer_capability,
+                               bool initiator, const u8 *extra_ies,
+                               size_t extra_ies_len, u8 oper_class,
+                               struct cfg80211_chan_def *chandef)
+{
+       struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+       struct sk_buff *skb = NULL;
+       struct sta_info *sta;
+       u32 flags = 0;
+       int ret = 0;
+
        rcu_read_lock();
        sta = sta_info_get(sdata, peer);
 
        /* infer the initiator if we can, to support old userspace */
        switch (action_code) {
        case WLAN_TDLS_SETUP_REQUEST:
-               if (sta)
+               if (sta) {
                        set_sta_flag(sta, WLAN_STA_TDLS_INITIATOR);
+                       sta->sta.tdls_initiator = false;
+               }
                /* fall-through */
        case WLAN_TDLS_SETUP_CONFIRM:
        case WLAN_TDLS_DISCOVERY_REQUEST:
@@ -575,13 +773,17 @@ ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
                 * Make the last packet sent take effect for the initiator
                 * value.
                 */
-               if (sta)
+               if (sta) {
                        clear_sta_flag(sta, WLAN_STA_TDLS_INITIATOR);
+                       sta->sta.tdls_initiator = true;
+               }
                /* fall-through */
        case WLAN_PUB_ACTION_TDLS_DISCOVER_RES:
                initiator = false;
                break;
        case WLAN_TDLS_TEARDOWN:
+       case WLAN_TDLS_CHANNEL_SWITCH_REQUEST:
+       case WLAN_TDLS_CHANNEL_SWITCH_RESPONSE:
                /* any value is ok */
                break;
        default:
@@ -596,9 +798,17 @@ ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
        if (ret < 0)
                goto fail;
 
-       ieee80211_tdls_add_ies(sdata, skb, peer, action_code, status_code,
-                              initiator, extra_ies, extra_ies_len);
-       if (send_direct) {
+       skb = ieee80211_tdls_build_mgmt_packet_data(sdata, peer, action_code,
+                                                   dialog_token, status_code,
+                                                   initiator, extra_ies,
+                                                   extra_ies_len, oper_class,
+                                                   chandef);
+       if (!skb) {
+               ret = -EINVAL;
+               goto fail;
+       }
+
+       if (action_code == WLAN_PUB_ACTION_TDLS_DISCOVER_RES) {
                ieee80211_tx_skb(sdata, skb);
                return 0;
        }
@@ -619,9 +829,44 @@ ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
                break;
        }
 
+       /*
+        * Set the WLAN_TDLS_TEARDOWN flag to indicate a teardown in progress.
+        * Later, if no ACK is returned from peer, we will re-send the teardown
+        * packet through the AP.
+        */
+       if ((action_code == WLAN_TDLS_TEARDOWN) &&
+           (sdata->local->hw.flags & IEEE80211_HW_REPORTS_TX_ACK_STATUS)) {
+               struct sta_info *sta = NULL;
+               bool try_resend; /* Should we keep skb for possible resend */
+
+               /* If not sending directly to peer - no point in keeping skb */
+               rcu_read_lock();
+               sta = sta_info_get(sdata, peer);
+               try_resend = sta && test_sta_flag(sta, WLAN_STA_TDLS_PEER_AUTH);
+               rcu_read_unlock();
+
+               spin_lock_bh(&sdata->u.mgd.teardown_lock);
+               if (try_resend && !sdata->u.mgd.teardown_skb) {
+                       /* Mark it as requiring TX status callback  */
+                       flags |= IEEE80211_TX_CTL_REQ_TX_STATUS |
+                                IEEE80211_TX_INTFL_MLME_CONN_TX;
+
+                       /*
+                        * skb is copied since mac80211 will later set
+                        * properties that might not be the same as the AP,
+                        * such as encryption, QoS, addresses, etc.
+                        *
+                        * No problem if skb_copy() fails, so no need to check.
+                        */
+                       sdata->u.mgd.teardown_skb = skb_copy(skb, GFP_ATOMIC);
+                       sdata->u.mgd.orig_teardown_skb = skb;
+               }
+               spin_unlock_bh(&sdata->u.mgd.teardown_lock);
+       }
+
        /* disable bottom halves when entering the Tx path */
        local_bh_disable();
-       ret = ieee80211_subif_start_xmit(skb, dev);
+       __ieee80211_subif_start_xmit(skb, dev, flags);
        local_bh_enable();
 
        return ret;
@@ -672,7 +917,8 @@ ieee80211_tdls_mgmt_setup(struct wiphy *wiphy, struct net_device *dev,
        ret = ieee80211_tdls_prep_mgmt_packet(wiphy, dev, peer, action_code,
                                              dialog_token, status_code,
                                              peer_capability, initiator,
-                                             extra_ies, extra_ies_len);
+                                             extra_ies, extra_ies_len, 0,
+                                             NULL);
        if (ret < 0)
                goto exit;
 
@@ -711,7 +957,8 @@ ieee80211_tdls_mgmt_teardown(struct wiphy *wiphy, struct net_device *dev,
        ret = ieee80211_tdls_prep_mgmt_packet(wiphy, dev, peer, action_code,
                                              dialog_token, status_code,
                                              peer_capability, initiator,
-                                             extra_ies, extra_ies_len);
+                                             extra_ies, extra_ies_len, 0,
+                                             NULL);
        if (ret < 0)
                sdata_err(sdata, "Failed sending TDLS teardown packet %d\n",
                          ret);
@@ -781,7 +1028,7 @@ int ieee80211_tdls_mgmt(struct wiphy *wiphy, struct net_device *dev,
                                                      status_code,
                                                      peer_capability,
                                                      initiator, extra_ies,
-                                                     extra_ies_len);
+                                                     extra_ies_len, 0, NULL);
                break;
        default:
                ret = -EOPNOTSUPP;
@@ -884,3 +1131,480 @@ void ieee80211_tdls_oper_request(struct ieee80211_vif *vif, const u8 *peer,
        cfg80211_tdls_oper_request(sdata->dev, peer, oper, reason_code, gfp);
 }
 EXPORT_SYMBOL(ieee80211_tdls_oper_request);
+
+static void
+iee80211_tdls_add_ch_switch_timing(u8 *buf, u16 switch_time, u16 switch_timeout)
+{
+       struct ieee80211_ch_switch_timing *ch_sw;
+
+       *buf++ = WLAN_EID_CHAN_SWITCH_TIMING;
+       *buf++ = sizeof(struct ieee80211_ch_switch_timing);
+
+       ch_sw = (void *)buf;
+       ch_sw->switch_time = cpu_to_le16(switch_time);
+       ch_sw->switch_timeout = cpu_to_le16(switch_timeout);
+}
+
+/* find switch timing IE in SKB ready for Tx */
+static const u8 *ieee80211_tdls_find_sw_timing_ie(struct sk_buff *skb)
+{
+       struct ieee80211_tdls_data *tf;
+       const u8 *ie_start;
+
+       /*
+        * Get the offset for the new location of the switch timing IE.
+        * The SKB network header will now point to the "payload_type"
+        * element of the TDLS data frame struct.
+        */
+       tf = container_of(skb->data + skb_network_offset(skb),
+                         struct ieee80211_tdls_data, payload_type);
+       ie_start = tf->u.chan_switch_req.variable;
+       return cfg80211_find_ie(WLAN_EID_CHAN_SWITCH_TIMING, ie_start,
+                               skb->len - (ie_start - skb->data));
+}
+
+static struct sk_buff *
+ieee80211_tdls_ch_sw_tmpl_get(struct sta_info *sta, u8 oper_class,
+                             struct cfg80211_chan_def *chandef,
+                             u32 *ch_sw_tm_ie_offset)
+{
+       struct ieee80211_sub_if_data *sdata = sta->sdata;
+       u8 extra_ies[2 + sizeof(struct ieee80211_sec_chan_offs_ie) +
+                    2 + sizeof(struct ieee80211_ch_switch_timing)];
+       int extra_ies_len = 2 + sizeof(struct ieee80211_ch_switch_timing);
+       u8 *pos = extra_ies;
+       struct sk_buff *skb;
+
+       /*
+        * if chandef points to a wide channel add a Secondary-Channel
+        * Offset information element
+        */
+       if (chandef->width == NL80211_CHAN_WIDTH_40) {
+               struct ieee80211_sec_chan_offs_ie *sec_chan_ie;
+               bool ht40plus;
+
+               *pos++ = WLAN_EID_SECONDARY_CHANNEL_OFFSET;
+               *pos++ = sizeof(*sec_chan_ie);
+               sec_chan_ie = (void *)pos;
+
+               ht40plus = cfg80211_get_chandef_type(chandef) ==
+                                                       NL80211_CHAN_HT40PLUS;
+               sec_chan_ie->sec_chan_offs = ht40plus ?
+                                            IEEE80211_HT_PARAM_CHA_SEC_ABOVE :
+                                            IEEE80211_HT_PARAM_CHA_SEC_BELOW;
+               pos += sizeof(*sec_chan_ie);
+
+               extra_ies_len += 2 + sizeof(struct ieee80211_sec_chan_offs_ie);
+       }
+
+       /* just set the values to 0, this is a template */
+       iee80211_tdls_add_ch_switch_timing(pos, 0, 0);
+
+       skb = ieee80211_tdls_build_mgmt_packet_data(sdata, sta->sta.addr,
+                                             WLAN_TDLS_CHANNEL_SWITCH_REQUEST,
+                                             0, 0, !sta->sta.tdls_initiator,
+                                             extra_ies, extra_ies_len,
+                                             oper_class, chandef);
+       if (!skb)
+               return NULL;
+
+       skb = ieee80211_build_data_template(sdata, skb, 0);
+       if (IS_ERR(skb)) {
+               tdls_dbg(sdata, "Failed building TDLS channel switch frame\n");
+               return NULL;
+       }
+
+       if (ch_sw_tm_ie_offset) {
+               const u8 *tm_ie = ieee80211_tdls_find_sw_timing_ie(skb);
+
+               if (!tm_ie) {
+                       tdls_dbg(sdata, "No switch timing IE in TDLS switch\n");
+                       dev_kfree_skb_any(skb);
+                       return NULL;
+               }
+
+               *ch_sw_tm_ie_offset = tm_ie - skb->data;
+       }
+
+       tdls_dbg(sdata,
+                "TDLS channel switch request template for %pM ch %d width %d\n",
+                sta->sta.addr, chandef->chan->center_freq, chandef->width);
+       return skb;
+}
+
+int
+ieee80211_tdls_channel_switch(struct wiphy *wiphy, struct net_device *dev,
+                             const u8 *addr, u8 oper_class,
+                             struct cfg80211_chan_def *chandef)
+{
+       struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+       struct ieee80211_local *local = sdata->local;
+       struct sta_info *sta;
+       struct sk_buff *skb = NULL;
+       u32 ch_sw_tm_ie;
+       int ret;
+
+       mutex_lock(&local->sta_mtx);
+       sta = sta_info_get(sdata, addr);
+       if (!sta) {
+               tdls_dbg(sdata,
+                        "Invalid TDLS peer %pM for channel switch request\n",
+                        addr);
+               ret = -ENOENT;
+               goto out;
+       }
+
+       if (!test_sta_flag(sta, WLAN_STA_TDLS_CHAN_SWITCH)) {
+               tdls_dbg(sdata, "TDLS channel switch unsupported by %pM\n",
+                        addr);
+               ret = -ENOTSUPP;
+               goto out;
+       }
+
+       skb = ieee80211_tdls_ch_sw_tmpl_get(sta, oper_class, chandef,
+                                           &ch_sw_tm_ie);
+       if (!skb) {
+               ret = -ENOENT;
+               goto out;
+       }
+
+       ret = drv_tdls_channel_switch(local, sdata, &sta->sta, oper_class,
+                                     chandef, skb, ch_sw_tm_ie);
+       if (!ret)
+               set_sta_flag(sta, WLAN_STA_TDLS_OFF_CHANNEL);
+
+out:
+       mutex_unlock(&local->sta_mtx);
+       dev_kfree_skb_any(skb);
+       return ret;
+}
+
+void
+ieee80211_tdls_cancel_channel_switch(struct wiphy *wiphy,
+                                    struct net_device *dev,
+                                    const u8 *addr)
+{
+       struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+       struct ieee80211_local *local = sdata->local;
+       struct sta_info *sta;
+
+       mutex_lock(&local->sta_mtx);
+       sta = sta_info_get(sdata, addr);
+       if (!sta) {
+               tdls_dbg(sdata,
+                        "Invalid TDLS peer %pM for channel switch cancel\n",
+                        addr);
+               goto out;
+       }
+
+       if (!test_sta_flag(sta, WLAN_STA_TDLS_OFF_CHANNEL)) {
+               tdls_dbg(sdata, "TDLS channel switch not initiated by %pM\n",
+                        addr);
+               goto out;
+       }
+
+       drv_tdls_cancel_channel_switch(local, sdata, &sta->sta);
+       clear_sta_flag(sta, WLAN_STA_TDLS_OFF_CHANNEL);
+
+out:
+       mutex_unlock(&local->sta_mtx);
+}
+
+static struct sk_buff *
+ieee80211_tdls_ch_sw_resp_tmpl_get(struct sta_info *sta,
+                                  u32 *ch_sw_tm_ie_offset)
+{
+       struct ieee80211_sub_if_data *sdata = sta->sdata;
+       struct sk_buff *skb;
+       u8 extra_ies[2 + sizeof(struct ieee80211_ch_switch_timing)];
+
+       /* initial timing are always zero in the template */
+       iee80211_tdls_add_ch_switch_timing(extra_ies, 0, 0);
+
+       skb = ieee80211_tdls_build_mgmt_packet_data(sdata, sta->sta.addr,
+                                       WLAN_TDLS_CHANNEL_SWITCH_RESPONSE,
+                                       0, 0, !sta->sta.tdls_initiator,
+                                       extra_ies, sizeof(extra_ies), 0, NULL);
+       if (!skb)
+               return NULL;
+
+       skb = ieee80211_build_data_template(sdata, skb, 0);
+       if (IS_ERR(skb)) {
+               tdls_dbg(sdata,
+                        "Failed building TDLS channel switch resp frame\n");
+               return NULL;
+       }
+
+       if (ch_sw_tm_ie_offset) {
+               const u8 *tm_ie = ieee80211_tdls_find_sw_timing_ie(skb);
+
+               if (!tm_ie) {
+                       tdls_dbg(sdata,
+                                "No switch timing IE in TDLS switch resp\n");
+                       dev_kfree_skb_any(skb);
+                       return NULL;
+               }
+
+               *ch_sw_tm_ie_offset = tm_ie - skb->data;
+       }
+
+       tdls_dbg(sdata, "TDLS get channel switch response template for %pM\n",
+                sta->sta.addr);
+       return skb;
+}
+
+static int
+ieee80211_process_tdls_channel_switch_resp(struct ieee80211_sub_if_data *sdata,
+                                          struct sk_buff *skb)
+{
+       struct ieee80211_local *local = sdata->local;
+       struct ieee802_11_elems elems;
+       struct sta_info *sta;
+       struct ieee80211_tdls_data *tf = (void *)skb->data;
+       bool local_initiator;
+       struct ieee80211_rx_status *rx_status = IEEE80211_SKB_RXCB(skb);
+       int baselen = offsetof(typeof(*tf), u.chan_switch_resp.variable);
+       struct ieee80211_tdls_ch_sw_params params = {};
+       int ret;
+
+       params.action_code = WLAN_TDLS_CHANNEL_SWITCH_RESPONSE;
+       params.timestamp = rx_status->device_timestamp;
+
+       if (skb->len < baselen) {
+               tdls_dbg(sdata, "TDLS channel switch resp too short: %d\n",
+                        skb->len);
+               return -EINVAL;
+       }
+
+       mutex_lock(&local->sta_mtx);
+       sta = sta_info_get(sdata, tf->sa);
+       if (!sta || !test_sta_flag(sta, WLAN_STA_TDLS_PEER_AUTH)) {
+               tdls_dbg(sdata, "TDLS chan switch from non-peer sta %pM\n",
+                        tf->sa);
+               ret = -EINVAL;
+               goto out;
+       }
+
+       params.sta = &sta->sta;
+       params.status = le16_to_cpu(tf->u.chan_switch_resp.status_code);
+       if (params.status != 0) {
+               ret = 0;
+               goto call_drv;
+       }
+
+       ieee802_11_parse_elems(tf->u.chan_switch_resp.variable,
+                              skb->len - baselen, false, &elems);
+       if (elems.parse_error) {
+               tdls_dbg(sdata, "Invalid IEs in TDLS channel switch resp\n");
+               ret = -EINVAL;
+               goto out;
+       }
+
+       if (!elems.ch_sw_timing || !elems.lnk_id) {
+               tdls_dbg(sdata, "TDLS channel switch resp - missing IEs\n");
+               ret = -EINVAL;
+               goto out;
+       }
+
+       /* validate the initiator is set correctly */
+       local_initiator =
+               !memcmp(elems.lnk_id->init_sta, sdata->vif.addr, ETH_ALEN);
+       if (local_initiator == sta->sta.tdls_initiator) {
+               tdls_dbg(sdata, "TDLS chan switch invalid lnk-id initiator\n");
+               ret = -EINVAL;
+               goto out;
+       }
+
+       params.switch_time = le16_to_cpu(elems.ch_sw_timing->switch_time);
+       params.switch_timeout = le16_to_cpu(elems.ch_sw_timing->switch_timeout);
+
+       params.tmpl_skb =
+               ieee80211_tdls_ch_sw_resp_tmpl_get(sta, &params.ch_sw_tm_ie);
+       if (!params.tmpl_skb) {
+               ret = -ENOENT;
+               goto out;
+       }
+
+call_drv:
+       drv_tdls_recv_channel_switch(sdata->local, sdata, &params);
+
+       tdls_dbg(sdata,
+                "TDLS channel switch response received from %pM status %d\n",
+                tf->sa, params.status);
+
+out:
+       mutex_unlock(&local->sta_mtx);
+       dev_kfree_skb_any(params.tmpl_skb);
+       return ret;
+}
+
+static int
+ieee80211_process_tdls_channel_switch_req(struct ieee80211_sub_if_data *sdata,
+                                         struct sk_buff *skb)
+{
+       struct ieee80211_local *local = sdata->local;
+       struct ieee802_11_elems elems;
+       struct cfg80211_chan_def chandef;
+       struct ieee80211_channel *chan;
+       enum nl80211_channel_type chan_type;
+       int freq;
+       u8 target_channel, oper_class;
+       bool local_initiator;
+       struct sta_info *sta;
+       enum ieee80211_band band;
+       struct ieee80211_tdls_data *tf = (void *)skb->data;
+       struct ieee80211_rx_status *rx_status = IEEE80211_SKB_RXCB(skb);
+       int baselen = offsetof(typeof(*tf), u.chan_switch_req.variable);
+       struct ieee80211_tdls_ch_sw_params params = {};
+       int ret = 0;
+
+       params.action_code = WLAN_TDLS_CHANNEL_SWITCH_REQUEST;
+       params.timestamp = rx_status->device_timestamp;
+
+       if (skb->len < baselen) {
+               tdls_dbg(sdata, "TDLS channel switch req too short: %d\n",
+                        skb->len);
+               return -EINVAL;
+       }
+
+       target_channel = tf->u.chan_switch_req.target_channel;
+       oper_class = tf->u.chan_switch_req.oper_class;
+
+       /*
+        * We can't easily infer the channel band. The operating class is
+        * ambiguous - there are multiple tables (US/Europe/JP/Global). The
+        * solution here is to treat channels with number >14 as 5GHz ones,
+        * and specifically check for the (oper_class, channel) combinations
+        * where this doesn't hold. These are thankfully unique according to
+        * IEEE802.11-2012.
+        * We consider only the 2GHz and 5GHz bands and 20MHz+ channels as
+        * valid here.
+        */
+       if ((oper_class == 112 || oper_class == 2 || oper_class == 3 ||
+            oper_class == 4 || oper_class == 5 || oper_class == 6) &&
+            target_channel < 14)
+               band = IEEE80211_BAND_5GHZ;
+       else
+               band = target_channel < 14 ? IEEE80211_BAND_2GHZ :
+                                            IEEE80211_BAND_5GHZ;
+
+       freq = ieee80211_channel_to_frequency(target_channel, band);
+       if (freq == 0) {
+               tdls_dbg(sdata, "Invalid channel in TDLS chan switch: %d\n",
+                        target_channel);
+               return -EINVAL;
+       }
+
+       chan = ieee80211_get_channel(sdata->local->hw.wiphy, freq);
+       if (!chan) {
+               tdls_dbg(sdata,
+                        "Unsupported channel for TDLS chan switch: %d\n",
+                        target_channel);
+               return -EINVAL;
+       }
+
+       ieee802_11_parse_elems(tf->u.chan_switch_req.variable,
+                              skb->len - baselen, false, &elems);
+       if (elems.parse_error) {
+               tdls_dbg(sdata, "Invalid IEs in TDLS channel switch req\n");
+               return -EINVAL;
+       }
+
+       if (!elems.ch_sw_timing || !elems.lnk_id) {
+               tdls_dbg(sdata, "TDLS channel switch req - missing IEs\n");
+               return -EINVAL;
+       }
+
+       mutex_lock(&local->sta_mtx);
+       sta = sta_info_get(sdata, tf->sa);
+       if (!sta || !test_sta_flag(sta, WLAN_STA_TDLS_PEER_AUTH)) {
+               tdls_dbg(sdata, "TDLS chan switch from non-peer sta %pM\n",
+                        tf->sa);
+               ret = -EINVAL;
+               goto out;
+       }
+
+       params.sta = &sta->sta;
+
+       /* validate the initiator is set correctly */
+       local_initiator =
+               !memcmp(elems.lnk_id->init_sta, sdata->vif.addr, ETH_ALEN);
+       if (local_initiator == sta->sta.tdls_initiator) {
+               tdls_dbg(sdata, "TDLS chan switch invalid lnk-id initiator\n");
+               ret = -EINVAL;
+               goto out;
+       }
+
+       if (!sta->sta.ht_cap.ht_supported) {
+               chan_type = NL80211_CHAN_NO_HT;
+       } else if (!elems.sec_chan_offs) {
+               chan_type = NL80211_CHAN_HT20;
+       } else {
+               switch (elems.sec_chan_offs->sec_chan_offs) {
+               case IEEE80211_HT_PARAM_CHA_SEC_ABOVE:
+                       chan_type = NL80211_CHAN_HT40PLUS;
+                       break;
+               case IEEE80211_HT_PARAM_CHA_SEC_BELOW:
+                       chan_type = NL80211_CHAN_HT40MINUS;
+                       break;
+               default:
+                       chan_type = NL80211_CHAN_HT20;
+                       break;
+               }
+       }
+
+       cfg80211_chandef_create(&chandef, chan, chan_type);
+       params.chandef = &chandef;
+
+       params.switch_time = le16_to_cpu(elems.ch_sw_timing->switch_time);
+       params.switch_timeout = le16_to_cpu(elems.ch_sw_timing->switch_timeout);
+
+       params.tmpl_skb =
+               ieee80211_tdls_ch_sw_resp_tmpl_get(sta,
+                                                  &params.ch_sw_tm_ie);
+       if (!params.tmpl_skb) {
+               ret = -ENOENT;
+               goto out;
+       }
+
+       drv_tdls_recv_channel_switch(sdata->local, sdata, &params);
+
+       tdls_dbg(sdata,
+                "TDLS ch switch request received from %pM ch %d width %d\n",
+                tf->sa, params.chandef->chan->center_freq,
+                params.chandef->width);
+out:
+       mutex_unlock(&local->sta_mtx);
+       dev_kfree_skb_any(params.tmpl_skb);
+       return ret;
+}
+
+void ieee80211_process_tdls_channel_switch(struct ieee80211_sub_if_data *sdata,
+                                          struct sk_buff *skb)
+{
+       struct ieee80211_tdls_data *tf = (void *)skb->data;
+       struct wiphy *wiphy = sdata->local->hw.wiphy;
+
+       /* make sure the driver supports it */
+       if (!(wiphy->features & NL80211_FEATURE_TDLS_CHANNEL_SWITCH))
+               return;
+
+       /* we want to access the entire packet */
+       if (skb_linearize(skb))
+               return;
+       /*
+        * The packet/size was already validated by mac80211 Rx path, only look
+        * at the action type.
+        */
+       switch (tf->action_code) {
+       case WLAN_TDLS_CHANNEL_SWITCH_REQUEST:
+               ieee80211_process_tdls_channel_switch_req(sdata, skb);
+               break;
+       case WLAN_TDLS_CHANNEL_SWITCH_RESPONSE:
+               ieee80211_process_tdls_channel_switch_resp(sdata, skb);
+               break;
+       default:
+               WARN_ON_ONCE(1);
+               return;
+       }
+}