diff options
Diffstat (limited to 'net/mac80211/offchannel.c')
| -rw-r--r-- | net/mac80211/offchannel.c | 428 | 
1 files changed, 368 insertions, 60 deletions
diff --git a/net/mac80211/offchannel.c b/net/mac80211/offchannel.c index 4b564091e51..7a17decd27f 100644 --- a/net/mac80211/offchannel.c +++ b/net/mac80211/offchannel.c @@ -12,12 +12,17 @@   * it under the terms of the GNU General Public License version 2 as   * published by the Free Software Foundation.   */ +#include <linux/export.h>  #include <net/mac80211.h>  #include "ieee80211_i.h" +#include "driver-ops.h"  /* - * inform AP that we will go to sleep so that it will buffer the frames - * while we scan + * Tell our hardware to disable PS. + * Optionally inform AP that we will go to sleep so that it will buffer + * the frames while we are doing off-channel work.  This is optional + * because we *may* be doing work on-operating channel, and want our + * hardware unconditionally awake, but still let the AP send us normal frames.   */  static void ieee80211_offchannel_ps_enable(struct ieee80211_sub_if_data *sdata)  { @@ -40,7 +45,7 @@ static void ieee80211_offchannel_ps_enable(struct ieee80211_sub_if_data *sdata)  		ieee80211_hw_config(local, IEEE80211_CONF_CHANGE_PS);  	} -	if (!(local->offchannel_ps_enabled) || +	if (!local->offchannel_ps_enabled ||  	    !(local->hw.flags & IEEE80211_HW_PS_NULLFUNC_STACK))  		/*  		 * If power save was enabled, no need to send a nullfunc @@ -76,6 +81,9 @@ static void ieee80211_offchannel_ps_disable(struct ieee80211_sub_if_data *sdata)  		 * we are sleeping, let's just enable power save mode in  		 * hardware.  		 */ +		/* TODO:  Only set hardware if CONF_PS changed? +		 * TODO:  Should we set offchannel_ps_enabled to false? +		 */  		local->hw.conf.flags |= IEEE80211_CONF_PS;  		ieee80211_hw_config(local, IEEE80211_CONF_CHANGE_PS);  	} else if (local->hw.conf.dynamic_ps_timeout > 0) { @@ -94,99 +102,399 @@ static void ieee80211_offchannel_ps_disable(struct ieee80211_sub_if_data *sdata)  	ieee80211_sta_reset_conn_monitor(sdata);  } -void ieee80211_offchannel_stop_beaconing(struct ieee80211_local *local) +void ieee80211_offchannel_stop_vifs(struct ieee80211_local *local)  {  	struct ieee80211_sub_if_data *sdata; +	if (WARN_ON(local->use_chanctx)) +		return; + +	/* +	 * notify the AP about us leaving the channel and stop all +	 * STA interfaces. +	 */ + +	/* +	 * Stop queues and transmit all frames queued by the driver +	 * before sending nullfunc to enable powersave at the AP. +	 */ +	ieee80211_stop_queues_by_reason(&local->hw, IEEE80211_MAX_QUEUE_MAP, +					IEEE80211_QUEUE_STOP_REASON_OFFCHANNEL); +	ieee80211_flush_queues(local, NULL); +  	mutex_lock(&local->iflist_mtx);  	list_for_each_entry(sdata, &local->interfaces, list) {  		if (!ieee80211_sdata_running(sdata))  			continue; -		/* disable beaconing */ -		if (sdata->vif.type == NL80211_IFTYPE_AP || -		    sdata->vif.type == NL80211_IFTYPE_ADHOC || -		    sdata->vif.type == NL80211_IFTYPE_MESH_POINT) -			ieee80211_bss_info_change_notify( -				sdata, BSS_CHANGED_BEACON_ENABLED); +		if (sdata->vif.type == NL80211_IFTYPE_P2P_DEVICE) +			continue; -		/* -		 * only handle non-STA interfaces here, STA interfaces -		 * are handled in ieee80211_offchannel_stop_station(), -		 * e.g., from the background scan state machine. -		 * -		 * In addition, do not stop monitor interface to allow it to be -		 * used from user space controlled off-channel operations. -		 */ -		if (sdata->vif.type != NL80211_IFTYPE_STATION && -		    sdata->vif.type != NL80211_IFTYPE_MONITOR) { +		if (sdata->vif.type != NL80211_IFTYPE_MONITOR)  			set_bit(SDATA_STATE_OFFCHANNEL, &sdata->state); -			netif_tx_stop_all_queues(sdata->dev); + +		/* Check to see if we should disable beaconing. */ +		if (sdata->vif.bss_conf.enable_beacon) { +			set_bit(SDATA_STATE_OFFCHANNEL_BEACON_STOPPED, +				&sdata->state); +			sdata->vif.bss_conf.enable_beacon = false; +			ieee80211_bss_info_change_notify( +				sdata, BSS_CHANGED_BEACON_ENABLED);  		} + +		if (sdata->vif.type == NL80211_IFTYPE_STATION && +		    sdata->u.mgd.associated) +			ieee80211_offchannel_ps_enable(sdata);  	}  	mutex_unlock(&local->iflist_mtx);  } -void ieee80211_offchannel_stop_station(struct ieee80211_local *local) +void ieee80211_offchannel_return(struct ieee80211_local *local)  {  	struct ieee80211_sub_if_data *sdata; -	/* -	 * notify the AP about us leaving the channel and stop all STA interfaces -	 */ +	if (WARN_ON(local->use_chanctx)) +		return; +  	mutex_lock(&local->iflist_mtx);  	list_for_each_entry(sdata, &local->interfaces, list) { +		if (sdata->vif.type == NL80211_IFTYPE_P2P_DEVICE) +			continue; + +		if (sdata->vif.type != NL80211_IFTYPE_MONITOR) +			clear_bit(SDATA_STATE_OFFCHANNEL, &sdata->state); +  		if (!ieee80211_sdata_running(sdata))  			continue; -		if (sdata->vif.type == NL80211_IFTYPE_STATION) { -			set_bit(SDATA_STATE_OFFCHANNEL, &sdata->state); -			netif_tx_stop_all_queues(sdata->dev); -			if (sdata->u.mgd.associated) -				ieee80211_offchannel_ps_enable(sdata); +		/* Tell AP we're back */ +		if (sdata->vif.type == NL80211_IFTYPE_STATION && +		    sdata->u.mgd.associated) +			ieee80211_offchannel_ps_disable(sdata); + +		if (test_and_clear_bit(SDATA_STATE_OFFCHANNEL_BEACON_STOPPED, +				       &sdata->state)) { +			sdata->vif.bss_conf.enable_beacon = true; +			ieee80211_bss_info_change_notify( +				sdata, BSS_CHANGED_BEACON_ENABLED);  		}  	}  	mutex_unlock(&local->iflist_mtx); + +	ieee80211_wake_queues_by_reason(&local->hw, IEEE80211_MAX_QUEUE_MAP, +					IEEE80211_QUEUE_STOP_REASON_OFFCHANNEL);  } -void ieee80211_offchannel_return(struct ieee80211_local *local, -				 bool enable_beaconing) +void ieee80211_handle_roc_started(struct ieee80211_roc_work *roc)  { -	struct ieee80211_sub_if_data *sdata; +	if (roc->notified) +		return; -	mutex_lock(&local->iflist_mtx); -	list_for_each_entry(sdata, &local->interfaces, list) { -		if (!ieee80211_sdata_running(sdata)) -			continue; +	if (roc->mgmt_tx_cookie) { +		if (!WARN_ON(!roc->frame)) { +			ieee80211_tx_skb_tid_band(roc->sdata, roc->frame, 7, +						  roc->chan->band); +			roc->frame = NULL; +		} +	} else { +		cfg80211_ready_on_channel(&roc->sdata->wdev, roc->cookie, +					  roc->chan, roc->req_duration, +					  GFP_KERNEL); +	} -		/* Tell AP we're back */ -		if (sdata->vif.type == NL80211_IFTYPE_STATION) { -			if (sdata->u.mgd.associated) -				ieee80211_offchannel_ps_disable(sdata); +	roc->notified = true; +} + +static void ieee80211_hw_roc_start(struct work_struct *work) +{ +	struct ieee80211_local *local = +		container_of(work, struct ieee80211_local, hw_roc_start); +	struct ieee80211_roc_work *roc, *dep, *tmp; + +	mutex_lock(&local->mtx); + +	if (list_empty(&local->roc_list)) +		goto out_unlock; + +	roc = list_first_entry(&local->roc_list, struct ieee80211_roc_work, +			       list); + +	if (!roc->started) +		goto out_unlock; + +	roc->hw_begun = true; +	roc->hw_start_time = local->hw_roc_start_time; + +	ieee80211_handle_roc_started(roc); +	list_for_each_entry_safe(dep, tmp, &roc->dependents, list) { +		ieee80211_handle_roc_started(dep); + +		if (dep->duration > roc->duration) { +			u32 dur = dep->duration; +			dep->duration = dur - roc->duration; +			roc->duration = dur; +			list_move(&dep->list, &roc->list);  		} +	} + out_unlock: +	mutex_unlock(&local->mtx); +} -		if (sdata->vif.type != NL80211_IFTYPE_MONITOR) { -			clear_bit(SDATA_STATE_OFFCHANNEL, &sdata->state); +void ieee80211_ready_on_channel(struct ieee80211_hw *hw) +{ +	struct ieee80211_local *local = hw_to_local(hw); + +	local->hw_roc_start_time = jiffies; + +	trace_api_ready_on_channel(local); + +	ieee80211_queue_work(hw, &local->hw_roc_start); +} +EXPORT_SYMBOL_GPL(ieee80211_ready_on_channel); + +void ieee80211_start_next_roc(struct ieee80211_local *local) +{ +	struct ieee80211_roc_work *roc; + +	lockdep_assert_held(&local->mtx); + +	if (list_empty(&local->roc_list)) { +		ieee80211_run_deferred_scan(local); +		return; +	} + +	roc = list_first_entry(&local->roc_list, struct ieee80211_roc_work, +			       list); + +	if (WARN_ON_ONCE(roc->started)) +		return; + +	if (local->ops->remain_on_channel) { +		int ret, duration = roc->duration; + +		/* XXX: duplicated, see ieee80211_start_roc_work() */ +		if (!duration) +			duration = 10; + +		ret = drv_remain_on_channel(local, roc->sdata, roc->chan, +					    duration, roc->type); + +		roc->started = true; + +		if (ret) { +			wiphy_warn(local->hw.wiphy, +				   "failed to start next HW ROC (%d)\n", ret);  			/* -			 * This may wake up queues even though the driver -			 * currently has them stopped. This is not very -			 * likely, since the driver won't have gotten any -			 * (or hardly any) new packets while we weren't -			 * on the right channel, and even if it happens -			 * it will at most lead to queueing up one more -			 * packet per queue in mac80211 rather than on -			 * the interface qdisc. +			 * queue the work struct again to avoid recursion +			 * when multiple failures occur  			 */ -			netif_tx_wake_all_queues(sdata->dev); +			ieee80211_remain_on_channel_expired(&local->hw);  		} +	} else { +		/* delay it a bit */ +		ieee80211_queue_delayed_work(&local->hw, &roc->work, +					     round_jiffies_relative(HZ/2)); +	} +} -		/* re-enable beaconing */ -		if (enable_beaconing && -		    (sdata->vif.type == NL80211_IFTYPE_AP || -		     sdata->vif.type == NL80211_IFTYPE_ADHOC || -		     sdata->vif.type == NL80211_IFTYPE_MESH_POINT)) -			ieee80211_bss_info_change_notify( -				sdata, BSS_CHANGED_BEACON_ENABLED); +void ieee80211_roc_notify_destroy(struct ieee80211_roc_work *roc, bool free) +{ +	struct ieee80211_roc_work *dep, *tmp; + +	if (WARN_ON(roc->to_be_freed)) +		return; + +	/* was never transmitted */ +	if (roc->frame) { +		cfg80211_mgmt_tx_status(&roc->sdata->wdev, +					(unsigned long)roc->frame, +					roc->frame->data, roc->frame->len, +					false, GFP_KERNEL); +		kfree_skb(roc->frame);  	} -	mutex_unlock(&local->iflist_mtx); + +	if (!roc->mgmt_tx_cookie) +		cfg80211_remain_on_channel_expired(&roc->sdata->wdev, +						   roc->cookie, roc->chan, +						   GFP_KERNEL); + +	list_for_each_entry_safe(dep, tmp, &roc->dependents, list) +		ieee80211_roc_notify_destroy(dep, true); + +	if (free) +		kfree(roc); +	else +		roc->to_be_freed = true; +} + +void ieee80211_sw_roc_work(struct work_struct *work) +{ +	struct ieee80211_roc_work *roc = +		container_of(work, struct ieee80211_roc_work, work.work); +	struct ieee80211_sub_if_data *sdata = roc->sdata; +	struct ieee80211_local *local = sdata->local; +	bool started, on_channel; + +	mutex_lock(&local->mtx); + +	if (roc->to_be_freed) +		goto out_unlock; + +	if (roc->abort) +		goto finish; + +	if (WARN_ON(list_empty(&local->roc_list))) +		goto out_unlock; + +	if (WARN_ON(roc != list_first_entry(&local->roc_list, +					    struct ieee80211_roc_work, +					    list))) +		goto out_unlock; + +	if (!roc->started) { +		struct ieee80211_roc_work *dep; + +		WARN_ON(local->use_chanctx); + +		/* If actually operating on the desired channel (with at least +		 * 20 MHz channel width) don't stop all the operations but still +		 * treat it as though the ROC operation started properly, so +		 * other ROC operations won't interfere with this one. +		 */ +		roc->on_channel = roc->chan == local->_oper_chandef.chan && +				  local->_oper_chandef.width != NL80211_CHAN_WIDTH_5 && +				  local->_oper_chandef.width != NL80211_CHAN_WIDTH_10; + +		/* start this ROC */ +		ieee80211_recalc_idle(local); + +		if (!roc->on_channel) { +			ieee80211_offchannel_stop_vifs(local); + +			local->tmp_channel = roc->chan; +			ieee80211_hw_config(local, 0); +		} + +		/* tell userspace or send frame */ +		ieee80211_handle_roc_started(roc); +		list_for_each_entry(dep, &roc->dependents, list) +			ieee80211_handle_roc_started(dep); + +		/* if it was pure TX, just finish right away */ +		if (!roc->duration) +			goto finish; + +		roc->started = true; +		ieee80211_queue_delayed_work(&local->hw, &roc->work, +					     msecs_to_jiffies(roc->duration)); +	} else { +		/* finish this ROC */ + finish: +		list_del(&roc->list); +		started = roc->started; +		on_channel = roc->on_channel; +		ieee80211_roc_notify_destroy(roc, !roc->abort); + +		if (started && !on_channel) { +			ieee80211_flush_queues(local, NULL); + +			local->tmp_channel = NULL; +			ieee80211_hw_config(local, 0); + +			ieee80211_offchannel_return(local); +		} + +		ieee80211_recalc_idle(local); + +		if (started) +			ieee80211_start_next_roc(local); +		else if (list_empty(&local->roc_list)) +			ieee80211_run_deferred_scan(local); +	} + + out_unlock: +	mutex_unlock(&local->mtx); +} + +static void ieee80211_hw_roc_done(struct work_struct *work) +{ +	struct ieee80211_local *local = +		container_of(work, struct ieee80211_local, hw_roc_done); +	struct ieee80211_roc_work *roc; + +	mutex_lock(&local->mtx); + +	if (list_empty(&local->roc_list)) +		goto out_unlock; + +	roc = list_first_entry(&local->roc_list, struct ieee80211_roc_work, +			       list); + +	if (!roc->started) +		goto out_unlock; + +	list_del(&roc->list); + +	ieee80211_roc_notify_destroy(roc, true); + +	/* if there's another roc, start it now */ +	ieee80211_start_next_roc(local); + + out_unlock: +	mutex_unlock(&local->mtx); +} + +void ieee80211_remain_on_channel_expired(struct ieee80211_hw *hw) +{ +	struct ieee80211_local *local = hw_to_local(hw); + +	trace_api_remain_on_channel_expired(local); + +	ieee80211_queue_work(hw, &local->hw_roc_done); +} +EXPORT_SYMBOL_GPL(ieee80211_remain_on_channel_expired); + +void ieee80211_roc_setup(struct ieee80211_local *local) +{ +	INIT_WORK(&local->hw_roc_start, ieee80211_hw_roc_start); +	INIT_WORK(&local->hw_roc_done, ieee80211_hw_roc_done); +	INIT_LIST_HEAD(&local->roc_list); +} + +void ieee80211_roc_purge(struct ieee80211_local *local, +			 struct ieee80211_sub_if_data *sdata) +{ +	struct ieee80211_roc_work *roc, *tmp; +	LIST_HEAD(tmp_list); + +	mutex_lock(&local->mtx); +	list_for_each_entry_safe(roc, tmp, &local->roc_list, list) { +		if (sdata && roc->sdata != sdata) +			continue; + +		if (roc->started && local->ops->remain_on_channel) { +			/* can race, so ignore return value */ +			drv_cancel_remain_on_channel(local); +		} + +		list_move_tail(&roc->list, &tmp_list); +		roc->abort = true; +	} +	mutex_unlock(&local->mtx); + +	list_for_each_entry_safe(roc, tmp, &tmp_list, list) { +		if (local->ops->remain_on_channel) { +			list_del(&roc->list); +			ieee80211_roc_notify_destroy(roc, true); +		} else { +			ieee80211_queue_delayed_work(&local->hw, &roc->work, 0); + +			/* work will clean up etc */ +			flush_delayed_work(&roc->work); +			WARN_ON(!roc->to_be_freed); +			kfree(roc); +		} +	} + +	WARN_ON_ONCE(!list_empty(&tmp_list));  }  | 
