From: Felix Fietkau <nbd@nbd.name>
Date: Thu, 14 Sep 2023 13:17:16 +0200
Subject: [PATCH] cfg80211: allow grace period for DFS available after beacon
 shutdown

In non-ETSI regulatory domains, DFS channel availability from completed CAC
expires after REG_PRE_CAC_EXPIRY_GRACE_MS (2 seconds) when no longer in use.
This creates a problem when reconfiguring an AP on a DFS channel: stopping
the AP to apply new settings causes the channel to immediately expire to
USABLE state, requiring a full CAC again.

The root cause is that the grace period timeout was calculated from
dfs_state_entered, which records when CAC completed. For an AP that has
been running for hours, this timestamp is far in the past, causing
immediate expiration.

Fix this by introducing a new field dfs_state_last_available that tracks
when a DFS channel was last actively used. This timestamp is updated:
- When DFS state transitions to AVAILABLE (CAC completion)
- When an AP stops beaconing on the channel
- When a channel switch moves away from the channel
- When DFS state is copied between wiphys

The grace period for AVAILABLE->USABLE transitions now uses this new
timestamp, giving a 2-second window to reconfigure the AP without losing
DFS availability.

Fixes: b35a51c7dd25 ("cfg80211: Make pre-CAC results valid only for ETSI domain")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---

--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -187,6 +187,8 @@ enum ieee80211_channel_flags {
  * @dfs_state: current state of this channel. Only relevant if radar is required
  *	on this channel.
  * @dfs_state_entered: timestamp (jiffies) when the dfs state was entered.
+ * @dfs_state_last_available: timestamp (jiffies) of the last time when the
+ *	channel was available.
  * @dfs_cac_ms: DFS CAC time in milliseconds, this is valid for DFS channels.
  * @psd: power spectral density (in dBm)
  */
@@ -204,6 +206,7 @@ struct ieee80211_channel {
 	int orig_mag, orig_mpwr;
 	enum nl80211_dfs_state dfs_state;
 	unsigned long dfs_state_entered;
+	unsigned long dfs_state_last_available;
 	unsigned int dfs_cac_ms;
 	s8 psd;
 };
--- a/net/wireless/ap.c
+++ b/net/wireless/ap.c
@@ -30,6 +30,9 @@ static int ___cfg80211_stop_ap(struct cf
 	if (!wdev->links[link_id].ap.beacon_interval)
 		return -ENOENT;
 
+	cfg80211_update_last_available(wdev->wiphy,
+				       &wdev->links[link_id].ap.chandef);
+
 	err = rdev_stop_ap(rdev, dev, link_id);
 	if (!err) {
 		wdev->conn_owner_nlportid = 0;
@@ -41,9 +44,6 @@ static int ___cfg80211_stop_ap(struct cf
 		if (notify)
 			nl80211_send_ap_stopped(wdev, link_id);
 
-		/* Should we apply the grace period during beaconing interface
-		 * shutdown also?
-		 */
 		cfg80211_sched_dfs_chan_update(rdev);
 	}
 
--- a/net/wireless/chan.c
+++ b/net/wireless/chan.c
@@ -639,6 +639,8 @@ void cfg80211_set_dfs_state(struct wiphy
 
 		c->dfs_state = dfs_state;
 		c->dfs_state_entered = jiffies;
+		if (dfs_state == NL80211_DFS_AVAILABLE)
+			c->dfs_state_last_available = jiffies;
 	}
 }
 
@@ -990,6 +992,28 @@ bool cfg80211_any_wiphy_oper_chan(struct
 	return false;
 }
 
+void cfg80211_update_last_available(struct wiphy *wiphy,
+				    const struct cfg80211_chan_def *chandef)
+{
+	struct ieee80211_channel *c;
+	int width;
+
+	if (WARN_ON(!cfg80211_chandef_valid(chandef)))
+		return;
+
+	width = cfg80211_chandef_get_width(chandef);
+	if (width < 0)
+		return;
+
+	for_each_subchan(chandef, freq, cf) {
+		c = ieee80211_get_channel_khz(wiphy, freq);
+		if (!c)
+			return;
+
+		c->dfs_state_last_available = jiffies;
+	}
+}
+
 static bool cfg80211_chandef_dfs_available(struct wiphy *wiphy,
 				const struct cfg80211_chan_def *chandef)
 {
--- a/net/wireless/core.h
+++ b/net/wireless/core.h
@@ -481,6 +481,8 @@ void cfg80211_set_dfs_state(struct wiphy
 			    enum nl80211_dfs_state dfs_state);
 
 void cfg80211_dfs_channels_update_work(struct work_struct *work);
+void cfg80211_update_last_available(struct wiphy *wiphy,
+				    const struct cfg80211_chan_def *chandef);
 
 void cfg80211_sched_dfs_chan_update(struct cfg80211_registered_device *rdev);
 
--- a/net/wireless/mlme.c
+++ b/net/wireless/mlme.c
@@ -1056,6 +1056,8 @@ void cfg80211_dfs_channels_update_work(s
 			if (c->dfs_state == NL80211_DFS_UNAVAILABLE) {
 				time_dfs_update = IEEE80211_DFS_MIN_NOP_TIME_MS;
 				radar_event = NL80211_RADAR_NOP_FINISHED;
+				timeout = c->dfs_state_entered +
+					  msecs_to_jiffies(time_dfs_update);
 			} else {
 				if (regulatory_pre_cac_allowed(wiphy) ||
 				    cfg80211_any_wiphy_oper_chan(wiphy, c))
@@ -1063,11 +1065,10 @@ void cfg80211_dfs_channels_update_work(s
 
 				time_dfs_update = REG_PRE_CAC_EXPIRY_GRACE_MS;
 				radar_event = NL80211_RADAR_PRE_CAC_EXPIRED;
+				timeout = c->dfs_state_last_available +
+					  msecs_to_jiffies(time_dfs_update);
 			}
 
-			timeout = c->dfs_state_entered +
-				  msecs_to_jiffies(time_dfs_update);
-
 			if (time_after_eq(jiffies, timeout)) {
 				c->dfs_state = NL80211_DFS_USABLE;
 				c->dfs_state_entered = jiffies;
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -20967,6 +20967,9 @@ void cfg80211_ch_switch_notify(struct ne
 		break;
 	case NL80211_IFTYPE_AP:
 	case NL80211_IFTYPE_P2P_GO:
+		if (wdev->links[link_id].ap.chandef.chan)
+			cfg80211_update_last_available(wdev->wiphy,
+						       &wdev->links[link_id].ap.chandef);
 		wdev->links[link_id].ap.chandef = *chandef;
 		break;
 	case NL80211_IFTYPE_ADHOC:
--- a/net/wireless/reg.c
+++ b/net/wireless/reg.c
@@ -2954,6 +2954,7 @@ static void reg_copy_dfs_chan_state(stru
 	    dst_chan->dfs_state == NL80211_DFS_USABLE) {
 		dst_chan->dfs_state = src_chan->dfs_state;
 		dst_chan->dfs_state_entered = src_chan->dfs_state_entered;
+		dst_chan->dfs_state_last_available = src_chan->dfs_state_last_available;
 	}
 }
 
