mac80211: support secondary channel offset in CSA

Add support for the secondary channel offset IE in channel
switch announcements. This is necessary for proper handling
of CSA on HT access points.

For this to work it is also necessary to convert everything
here to use chandef structs instead of just channels. The
driver updates aren't really correct though. In particular,
the TI wl18xx driver update can't possibly be right since
it just ignores the new channel width for lack of firmware
API.

Signed-off-by: Johannes Berg <johannes.berg@intel.com>
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 10c3180..8f240c0 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -1019,7 +1019,7 @@
 	enum mac80211_scan_state next_scan_state;
 	struct delayed_work scan_work;
 	struct ieee80211_sub_if_data __rcu *scan_sdata;
-	struct ieee80211_channel *csa_channel;
+	struct cfg80211_chan_def csa_chandef;
 	/* For backward compatibility only -- do not use */
 	struct cfg80211_chan_def _oper_chandef;
 
@@ -1183,6 +1183,7 @@
 	const u8 *pwr_constr_elem;
 	const struct ieee80211_timeout_interval_ie *timeout_int;
 	const u8 *opmode_notif;
+	const struct ieee80211_sec_chan_offs_ie *sec_chan_offs;
 
 	/* length of them, respectively */
 	u8 ssid_len;
diff --git a/net/mac80211/mlme.c b/net/mac80211/mlme.c
index bc6f87e..bd581a8 100644
--- a/net/mac80211/mlme.c
+++ b/net/mac80211/mlme.c
@@ -289,6 +289,8 @@
 	} else {
 		/* 40 MHz (and 80 MHz) must be supported for VHT */
 		ret = IEEE80211_STA_DISABLE_VHT;
+		/* also mark 40 MHz disabled */
+		ret |= IEEE80211_STA_DISABLE_40MHZ;
 		goto out;
 	}
 
@@ -964,16 +966,7 @@
 	if (!ifmgd->associated)
 		goto out;
 
-	/*
-	 * FIXME: Here we are downgrading to NL80211_CHAN_WIDTH_20_NOHT
-	 * and don't adjust our ht/vht settings
-	 * This is wrong - we should behave according to the CSA params
-	 */
-	local->_oper_chandef.chan = local->csa_channel;
-	local->_oper_chandef.width = NL80211_CHAN_WIDTH_20_NOHT;
-	local->_oper_chandef.center_freq1 =
-		local->_oper_chandef.chan->center_freq;
-	local->_oper_chandef.center_freq2 = 0;
+	local->_oper_chandef = local->csa_chandef;
 
 	if (!local->ops->channel_switch) {
 		/* call "hw_config" only if doing sw channel switch */
@@ -1028,13 +1021,14 @@
 	struct ieee80211_if_managed *ifmgd = &sdata->u.mgd;
 	struct cfg80211_bss *cbss = ifmgd->associated;
 	struct ieee80211_bss *bss;
-	struct ieee80211_channel *new_ch;
 	struct ieee80211_chanctx *chanctx;
 	enum ieee80211_band new_band;
 	int new_freq;
 	u8 new_chan_no;
 	u8 count;
 	u8 mode;
+	struct cfg80211_chan_def new_chandef = {};
+	int secondary_channel_offset = -1;
 
 	ASSERT_MGD_MTX(ifmgd);
 
@@ -1048,6 +1042,19 @@
 	if (ifmgd->flags & IEEE80211_STA_CSA_RECEIVED)
 		return;
 
+	if (!(ifmgd->flags & IEEE80211_STA_DISABLE_HT)) {
+		/* if HT is enabled and the IE not present, it's still HT */
+		secondary_channel_offset = IEEE80211_HT_PARAM_CHA_SEC_NONE;
+		if (elems->sec_chan_offs)
+			secondary_channel_offset =
+				elems->sec_chan_offs->sec_chan_offs;
+	}
+
+	if (ifmgd->flags & IEEE80211_STA_DISABLE_40MHZ &&
+	    (secondary_channel_offset == IEEE80211_HT_PARAM_CHA_SEC_ABOVE ||
+	     secondary_channel_offset == IEEE80211_HT_PARAM_CHA_SEC_BELOW))
+		secondary_channel_offset = IEEE80211_HT_PARAM_CHA_SEC_NONE;
+
 	if (elems->ext_chansw_ie) {
 		if (!ieee80211_operating_class_to_band(
 				elems->ext_chansw_ie->new_operating_class,
@@ -1074,8 +1081,9 @@
 	bss = (void *)cbss->priv;
 
 	new_freq = ieee80211_channel_to_frequency(new_chan_no, new_band);
-	new_ch = ieee80211_get_channel(local->hw.wiphy, new_freq);
-	if (!new_ch || new_ch->flags & IEEE80211_CHAN_DISABLED) {
+	new_chandef.chan = ieee80211_get_channel(sdata->local->hw.wiphy, new_freq);
+	if (!new_chandef.chan ||
+	    new_chandef.chan->flags & IEEE80211_CHAN_DISABLED) {
 		sdata_info(sdata,
 			   "AP %pM switches to unsupported channel (%d MHz), disconnecting\n",
 			   ifmgd->associated->bssid, new_freq);
@@ -1084,6 +1092,39 @@
 		return;
 	}
 
+	switch (secondary_channel_offset) {
+	default:
+		/* secondary_channel_offset was present but is invalid */
+	case IEEE80211_HT_PARAM_CHA_SEC_NONE:
+		cfg80211_chandef_create(&new_chandef, new_chandef.chan,
+					NL80211_CHAN_HT20);
+		break;
+	case IEEE80211_HT_PARAM_CHA_SEC_ABOVE:
+		cfg80211_chandef_create(&new_chandef, new_chandef.chan,
+					NL80211_CHAN_HT40PLUS);
+		break;
+	case IEEE80211_HT_PARAM_CHA_SEC_BELOW:
+		cfg80211_chandef_create(&new_chandef, new_chandef.chan,
+					NL80211_CHAN_HT40MINUS);
+		break;
+	case -1:
+		cfg80211_chandef_create(&new_chandef, new_chandef.chan,
+					NL80211_CHAN_NO_HT);
+		break;
+	}
+
+	if (!cfg80211_chandef_usable(local->hw.wiphy, &new_chandef,
+				     IEEE80211_CHAN_DISABLED)) {
+		sdata_info(sdata,
+			   "AP %pM switches to unsupported channel (%d MHz, width:%d, CF1/2: %d/%d MHz), disconnecting\n",
+			   ifmgd->associated->bssid, new_freq,
+			   new_chandef.width, new_chandef.center_freq1,
+			   new_chandef.center_freq2);
+		ieee80211_queue_work(&local->hw,
+				     &ifmgd->csa_connection_drop_work);
+		return;
+	}
+
 	ifmgd->flags |= IEEE80211_STA_CSA_RECEIVED;
 
 	if (local->use_chanctx) {
@@ -1111,7 +1152,7 @@
 	}
 	mutex_unlock(&local->chanctx_mtx);
 
-	local->csa_channel = new_ch;
+	local->csa_chandef = new_chandef;
 
 	if (mode)
 		ieee80211_stop_queues_by_reason(&local->hw,
@@ -1123,7 +1164,7 @@
 		struct ieee80211_channel_switch ch_switch = {
 			.timestamp = timestamp,
 			.block_tx = mode,
-			.channel = new_ch,
+			.chandef = new_chandef,
 			.count = count,
 		};
 
diff --git a/net/mac80211/trace.h b/net/mac80211/trace.h
index 8286dce..c215fafd7 100644
--- a/net/mac80211/trace.h
+++ b/net/mac80211/trace.h
@@ -990,23 +990,23 @@
 
 	TP_STRUCT__entry(
 		LOCAL_ENTRY
+		CHANDEF_ENTRY
 		__field(u64, timestamp)
 		__field(bool, block_tx)
-		__field(u16, freq)
 		__field(u8, count)
 	),
 
 	TP_fast_assign(
 		LOCAL_ASSIGN;
+		CHANDEF_ASSIGN(&ch_switch->chandef)
 		__entry->timestamp = ch_switch->timestamp;
 		__entry->block_tx = ch_switch->block_tx;
-		__entry->freq = ch_switch->channel->center_freq;
 		__entry->count = ch_switch->count;
 	),
 
 	TP_printk(
-		LOCAL_PR_FMT " new freq:%u count:%d",
-		LOCAL_PR_ARG, __entry->freq, __entry->count
+		LOCAL_PR_FMT " new " CHANDEF_PR_FMT " count:%d",
+		LOCAL_PR_ARG, CHANDEF_PR_ARG, __entry->count
 	)
 );
 
diff --git a/net/mac80211/util.c b/net/mac80211/util.c
index e4a6d55..155056c 100644
--- a/net/mac80211/util.c
+++ b/net/mac80211/util.c
@@ -716,6 +716,7 @@
 		case WLAN_EID_COUNTRY:
 		case WLAN_EID_PWR_CONSTRAINT:
 		case WLAN_EID_TIMEOUT_INTERVAL:
+		case WLAN_EID_SECONDARY_CHANNEL_OFFSET:
 			if (test_bit(id, seen_elems)) {
 				elems->parse_error = true;
 				left -= elen;
@@ -870,6 +871,13 @@
 			}
 			elems->ext_chansw_ie = (void *)pos;
 			break;
+		case WLAN_EID_SECONDARY_CHANNEL_OFFSET:
+			if (elen != sizeof(struct ieee80211_sec_chan_offs_ie)) {
+				elem_parse_failed = true;
+				break;
+			}
+			elems->sec_chan_offs = (void *)pos;
+			break;
 		case WLAN_EID_COUNTRY:
 			elems->country_elem = pos;
 			elems->country_elem_len = elen;