mac80211: introduce TDLS channel switch ops
authorArik Nemtsov <arik@wizery.com>
Sun, 9 Nov 2014 16:50:19 +0000 (18:50 +0200)
committerJohannes Berg <johannes.berg@intel.com>
Wed, 19 Nov 2014 17:45:21 +0000 (18:45 +0100)
Implement the cfg80211 TDLS channel switch ops and introduce new mac80211
ones for low-level drivers.
Verify low-level driver support for the new ops when using the relevant
wiphy feature bit. Also verify the peer supports channel switching before
passing the command down.

Add a new STA flag to track the off-channel state with the TDLS peer and
make sure to cancel the channel-switch if the peer STA is unexpectedly
removed.

Signed-off-by: Arik Nemtsov <arikx.nemtsov@intel.com>
Signed-off-by: Arik Nemtsov <arik@wizery.com>
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
include/net/mac80211.h
net/mac80211/cfg.c
net/mac80211/debugfs_sta.c
net/mac80211/driver-ops.h
net/mac80211/ieee80211_i.h
net/mac80211/main.c
net/mac80211/sta_info.c
net/mac80211/sta_info.h
net/mac80211/tdls.c
net/mac80211/trace.h

index 83232aa..fdedceb 100644 (file)
@@ -2915,6 +2915,16 @@ enum ieee80211_reconfig_type {
  *
  * @get_txpower: get current maximum tx power (in dBm) based on configuration
  *     and hardware limits.
+ *
+ * @tdls_channel_switch: Start channel-switching with a TDLS peer. The driver
+ *     is responsible for continually initiating channel-switching operations
+ *     and returning to the base channel for communication with the AP. The
+ *     driver receives a channel-switch request template and the location of
+ *     the switch-timing IE within the template as part of the invocation.
+ *     The template is valid only within the call, and the driver can
+ *     optionally copy the skb for further re-use.
+ * @tdls_cancel_channel_switch: Stop channel-switching with a TDLS peer. Both
+ *     peers must be on the base channel when the call completes.
  */
 struct ieee80211_ops {
        void (*tx)(struct ieee80211_hw *hw,
@@ -3126,6 +3136,15 @@ struct ieee80211_ops {
        u32 (*get_expected_throughput)(struct ieee80211_sta *sta);
        int (*get_txpower)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
                           int *dbm);
+
+       int (*tdls_channel_switch)(struct ieee80211_hw *hw,
+                                  struct ieee80211_vif *vif,
+                                  struct ieee80211_sta *sta, u8 oper_class,
+                                  struct cfg80211_chan_def *chandef,
+                                  struct sk_buff *skb, u32 ch_sw_tm_ie);
+       void (*tdls_cancel_channel_switch)(struct ieee80211_hw *hw,
+                                          struct ieee80211_vif *vif,
+                                          struct ieee80211_sta *sta);
 };
 
 /**
index 8195e65..e75d5c5 100644 (file)
@@ -3752,6 +3752,8 @@ const struct cfg80211_ops mac80211_config_ops = {
        .set_rekey_data = ieee80211_set_rekey_data,
        .tdls_oper = ieee80211_tdls_oper,
        .tdls_mgmt = ieee80211_tdls_mgmt,
+       .tdls_channel_switch = ieee80211_tdls_channel_switch,
+       .tdls_cancel_channel_switch = ieee80211_tdls_cancel_channel_switch,
        .probe_client = ieee80211_probe_client,
        .set_noack_map = ieee80211_set_noack_map,
 #ifdef CONFIG_PM
index 2ba7f53..94c7009 100644 (file)
@@ -74,7 +74,7 @@ static ssize_t sta_flags_read(struct file *file, char __user *userbuf,
        test_sta_flag(sta, WLAN_STA_##flg) ? #flg "\n" : ""
 
        int res = scnprintf(buf, sizeof(buf),
-                           "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
+                           "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
                            TEST(AUTH), TEST(ASSOC), TEST(PS_STA),
                            TEST(PS_DRIVER), TEST(AUTHORIZED),
                            TEST(SHORT_PREAMBLE),
@@ -83,10 +83,10 @@ static ssize_t sta_flags_read(struct file *file, char __user *userbuf,
                            TEST(MFP), TEST(BLOCK_BA), TEST(PSPOLL),
                            TEST(UAPSD), TEST(SP), TEST(TDLS_PEER),
                            TEST(TDLS_PEER_AUTH), TEST(TDLS_INITIATOR),
-                           TEST(TDLS_CHAN_SWITCH), TEST(4ADDR_EVENT),
-                           TEST(INSERTED), TEST(RATE_CONTROL),
-                           TEST(TOFFSET_KNOWN), TEST(MPSP_OWNER),
-                           TEST(MPSP_RECIPIENT));
+                           TEST(TDLS_CHAN_SWITCH), TEST(TDLS_OFF_CHANNEL),
+                           TEST(4ADDR_EVENT), TEST(INSERTED),
+                           TEST(RATE_CONTROL), TEST(TOFFSET_KNOWN),
+                           TEST(MPSP_OWNER), TEST(MPSP_RECIPIENT));
 #undef TEST
        return simple_read_from_buffer(userbuf, count, ppos, buf, res);
 }
index 9759dd1..ec4ae42 100644 (file)
@@ -1296,4 +1296,45 @@ static inline int drv_get_txpower(struct ieee80211_local *local,
        return ret;
 }
 
+static inline int
+drv_tdls_channel_switch(struct ieee80211_local *local,
+                       struct ieee80211_sub_if_data *sdata,
+                       struct ieee80211_sta *sta, u8 oper_class,
+                       struct cfg80211_chan_def *chandef,
+                       struct sk_buff *tmpl_skb, u32 ch_sw_tm_ie)
+{
+       int ret;
+
+       might_sleep();
+       if (!check_sdata_in_driver(sdata))
+               return -EIO;
+
+       if (!local->ops->tdls_channel_switch)
+               return -EOPNOTSUPP;
+
+       trace_drv_tdls_channel_switch(local, sdata, sta, oper_class, chandef);
+       ret = local->ops->tdls_channel_switch(&local->hw, &sdata->vif, sta,
+                                             oper_class, chandef, tmpl_skb,
+                                             ch_sw_tm_ie);
+       trace_drv_return_int(local, ret);
+       return ret;
+}
+
+static inline void
+drv_tdls_cancel_channel_switch(struct ieee80211_local *local,
+                              struct ieee80211_sub_if_data *sdata,
+                              struct ieee80211_sta *sta)
+{
+       might_sleep();
+       if (!check_sdata_in_driver(sdata))
+               return;
+
+       if (!local->ops->tdls_cancel_channel_switch)
+               return;
+
+       trace_drv_tdls_cancel_channel_switch(local, sdata, sta);
+       local->ops->tdls_cancel_channel_switch(&local->hw, &sdata->vif, sta);
+       trace_drv_return_void(local);
+}
+
 #endif /* __MAC80211_DRIVER_OPS */
index e786ab6..2c7abc0 100644 (file)
@@ -2007,6 +2007,12 @@ int ieee80211_tdls_mgmt(struct wiphy *wiphy, struct net_device *dev,
 int ieee80211_tdls_oper(struct wiphy *wiphy, struct net_device *dev,
                        const u8 *peer, enum nl80211_tdls_operation oper);
 void ieee80211_tdls_peer_del_work(struct work_struct *wk);
+int ieee80211_tdls_channel_switch(struct wiphy *wiphy, struct net_device *dev,
+                                 const u8 *addr, u8 oper_class,
+                                 struct cfg80211_chan_def *chandef);
+void ieee80211_tdls_cancel_channel_switch(struct wiphy *wiphy,
+                                         struct net_device *dev,
+                                         const u8 *addr);
 
 extern const struct ethtool_ops ieee80211_ethtool_ops;
 
index 282a4f3..774ccb2 100644 (file)
@@ -764,6 +764,11 @@ int ieee80211_register_hw(struct ieee80211_hw *hw)
             local->hw.offchannel_tx_hw_queue >= local->hw.queues))
                return -EINVAL;
 
+       if ((hw->wiphy->features & NL80211_FEATURE_TDLS_CHANNEL_SWITCH) &&
+           (!local->ops->tdls_channel_switch ||
+            !local->ops->tdls_cancel_channel_switch))
+               return -EOPNOTSUPP;
+
 #ifdef CONFIG_PM
        if (hw->wiphy->wowlan && (!local->ops->suspend || !local->ops->resume))
                return -EINVAL;
index 9737251..86ca627 100644 (file)
@@ -847,6 +847,15 @@ static int __must_check __sta_info_destroy_part1(struct sta_info *sta)
        if (WARN_ON(ret))
                return ret;
 
+       /*
+        * for TDLS peers, make sure to return to the base channel before
+        * removal.
+        */
+       if (test_sta_flag(sta, WLAN_STA_TDLS_OFF_CHANNEL)) {
+               drv_tdls_cancel_channel_switch(local, sdata, &sta->sta);
+               clear_sta_flag(sta, WLAN_STA_TDLS_OFF_CHANNEL);
+       }
+
        list_del_rcu(&sta->list);
 
        drv_sta_pre_rcu_remove(local, sta->sdata, sta);
index b6702c8..00f56eb 100644 (file)
@@ -50,6 +50,8 @@
  * @WLAN_STA_TDLS_INITIATOR: We are the initiator of the TDLS link with this
  *     station.
  * @WLAN_STA_TDLS_CHAN_SWITCH: This TDLS peer supports TDLS channel-switching
+ * @WLAN_STA_TDLS_OFF_CHANNEL: The local STA is currently off-channel with this
+ *     TDLS peer
  * @WLAN_STA_UAPSD: Station requested unscheduled SP while driver was
  *     keeping station in power-save mode, reply when the driver
  *     unblocks the station.
@@ -80,6 +82,7 @@ enum ieee80211_sta_info_flags {
        WLAN_STA_TDLS_PEER_AUTH,
        WLAN_STA_TDLS_INITIATOR,
        WLAN_STA_TDLS_CHAN_SWITCH,
+       WLAN_STA_TDLS_OFF_CHANNEL,
        WLAN_STA_UAPSD,
        WLAN_STA_SP,
        WLAN_STA_4ADDR_EVENT,
index fa141ae..358f9a4 100644 (file)
@@ -449,6 +449,48 @@ 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_ies(struct ieee80211_sub_if_data *sdata,
                                   struct sk_buff *skb, const u8 *peer,
                                   u8 action_code, u16 status_code,
@@ -481,6 +523,12 @@ 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;
        }
 
 }
@@ -547,6 +595,12 @@ 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;
        default:
                return -EINVAL;
        }
@@ -626,6 +680,7 @@ ieee80211_tdls_build_mgmt_packet_data(struct ieee80211_sub_if_data *sdata,
        case WLAN_TDLS_SETUP_CONFIRM:
        case WLAN_TDLS_TEARDOWN:
        case WLAN_TDLS_DISCOVERY_REQUEST:
+       case WLAN_TDLS_CHANNEL_SWITCH_REQUEST:
                ret = ieee80211_prep_tdls_encap_data(local->hw.wiphy,
                                                     sdata->dev, peer,
                                                     action_code, dialog_token,
@@ -699,6 +754,7 @@ ieee80211_tdls_prep_mgmt_packet(struct wiphy *wiphy, struct net_device *dev,
                initiator = false;
                break;
        case WLAN_TDLS_TEARDOWN:
+       case WLAN_TDLS_CHANNEL_SWITCH_REQUEST:
                /* any value is ok */
                break;
        default:
@@ -1046,3 +1102,181 @@ 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);
+}
index 96847e7..c0c0fca 100644 (file)
@@ -2196,6 +2196,63 @@ TRACE_EVENT(drv_get_txpower,
        )
 );
 
+TRACE_EVENT(drv_tdls_channel_switch,
+       TP_PROTO(struct ieee80211_local *local,
+                struct ieee80211_sub_if_data *sdata,
+                struct ieee80211_sta *sta, u8 oper_class,
+                struct cfg80211_chan_def *chandef),
+
+       TP_ARGS(local, sdata, sta, oper_class, chandef),
+
+       TP_STRUCT__entry(
+               LOCAL_ENTRY
+               VIF_ENTRY
+               STA_ENTRY
+               __field(u8, oper_class)
+               CHANDEF_ENTRY
+       ),
+
+       TP_fast_assign(
+               LOCAL_ASSIGN;
+               VIF_ASSIGN;
+               STA_ASSIGN;
+               __entry->oper_class = oper_class;
+               CHANDEF_ASSIGN(chandef)
+       ),
+
+       TP_printk(
+               LOCAL_PR_FMT VIF_PR_FMT " tdls channel switch to"
+               CHANDEF_PR_FMT  " oper_class:%d " STA_PR_FMT,
+               LOCAL_PR_ARG, VIF_PR_ARG, CHANDEF_PR_ARG, __entry->oper_class,
+               STA_PR_ARG
+       )
+);
+
+TRACE_EVENT(drv_tdls_cancel_channel_switch,
+       TP_PROTO(struct ieee80211_local *local,
+                struct ieee80211_sub_if_data *sdata,
+                struct ieee80211_sta *sta),
+
+       TP_ARGS(local, sdata, sta),
+
+       TP_STRUCT__entry(
+               LOCAL_ENTRY
+               VIF_ENTRY
+               STA_ENTRY
+       ),
+
+       TP_fast_assign(
+               LOCAL_ASSIGN;
+               VIF_ASSIGN;
+               STA_ASSIGN;
+       ),
+
+       TP_printk(
+               LOCAL_PR_FMT VIF_PR_FMT
+               " tdls cancel channel switch with " STA_PR_FMT,
+               LOCAL_PR_ARG, VIF_PR_ARG, STA_PR_ARG
+       )
+);
 
 #ifdef CONFIG_MAC80211_MESSAGE_TRACING
 #undef TRACE_SYSTEM