[PATCH usteer 2/2] policy: add station policy exemption list

AndreaCovelli andcov23 at gmail.com
Sun May 3 16:17:28 PDT 2026


Allow users to configure station MAC addresses that should be exempt
from steering policy decisions.

Infrastructure links such as WDS or Multi-AP backhaul stations can
be associated to the same ESS as normal clients. Steering or kicking
those stations can disrupt downstream AP connectivity, while splitting
the backhaul SSID is not always desirable.

Add a policy_exempt_sta_list config array and skip matching stations
in request steering, roam transitions, delayed kicks, SNR/load kicks,
link measurement requests and BSS transition query handling. Keep exempt
stations tracked and visible in ubus status output.

Signed-off-by: AndreaCovelli <andcov23 at gmail.com>
---
Build-tested with CMake against locally built libubox/ubus dependencies.
Build-tested with OpenWrt SDK 25.12.2 mediatek/filogic for Cudy WR3000P
together with patch 1/2.
Runtime-tested on a Cudy WR3000P v1 with a WDS station in
policy_exempt_sta_list. Under aggressive roam scan settings, ubus reported
policy_exempt=true for the WDS station and no rrm_beacon_req was sent to it,
while non-exempt clients still received beacon requests.
Related to GitHub PR #11, but keeps policy checks at the ubus action layer
as a final guard for exempt stations.

 local_node.c                           |  6 ++++
 openwrt/usteer/files/etc/config/usteer |  4 +++
 openwrt/usteer/files/etc/init.d/usteer |  1 +
 policy.c                               | 19 ++++++++++
 sta.c                                  | 45 +++++++++++++++++++++++
 ubus.c                                 | 50 ++++++++++++++++++++++----
 usteer.h                               |  5 +++
 7 files changed, 124 insertions(+), 6 deletions(-)

diff --git a/local_node.c b/local_node.c
index e74d945..e89e843 100644
--- a/local_node.c
+++ b/local_node.c
@@ -694,6 +694,9 @@ usteer_local_node_request_link_measurement(struct usteer_local_node *ln)
 		if (si->connected != STA_CONNECTED)
 			continue;
 
+		if (usteer_sta_is_policy_exempt(si->sta->addr))
+			continue;
+
 		usteer_ubus_trigger_link_measurement(si);
 	}
 }
@@ -744,6 +747,9 @@ usteer_local_node_process_bss_tm_queries(struct uloop_timeout *timeout)
 		if (!sta)
 			continue;
 
+		if (usteer_sta_is_policy_exempt(sta->addr))
+			continue;
+
 		si = usteer_sta_info_get(sta, node, false);
 		if (!si)
 			continue;
diff --git a/openwrt/usteer/files/etc/config/usteer b/openwrt/usteer/files/etc/config/usteer
index f53c338..43f414f 100644
--- a/openwrt/usteer/files/etc/config/usteer
+++ b/openwrt/usteer/files/etc/config/usteer
@@ -153,3 +153,7 @@ config usteer
 
 	# List of SSIDs to enable steering on
 	#list ssid_list ''
+
+	# List of station MAC addresses to exempt from steering policy decisions,
+	# including automated kicks
+	#list policy_exempt_sta_list ''
diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer
index 07fd99e..05a3f91 100755
--- a/openwrt/usteer/files/etc/init.d/usteer
+++ b/openwrt/usteer/files/etc/init.d/usteer
@@ -71,6 +71,7 @@ uci_usteer() {
 	uci_option_to_json_bool "$cfg" assoc_steering
 	uci_option_to_json_string "$cfg" node_up_script
 	uci_option_to_json_string_array "$cfg" ssid_list
+	uci_option_to_json_string_array "$cfg" policy_exempt_sta_list
 	uci_option_to_json_string_array "$cfg" event_log_types
 
 	for opt in \
diff --git a/policy.c b/policy.c
index 8c5d244..d047106 100644
--- a/policy.c
+++ b/policy.c
@@ -176,6 +176,9 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type)
 	int min_signal;
 	bool ret = true;
 
+	if (usteer_sta_is_policy_exempt(si->sta->addr))
+		goto out;
+
 	if (type == EVENT_TYPE_PROBE && !config.probe_steering)
 		goto out;
 
@@ -381,6 +384,9 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si)
 
 bool usteer_policy_can_perform_roam(struct sta_info *si)
 {
+	if (usteer_sta_is_policy_exempt(si->sta->addr))
+		return false;
+
 	/* Only trigger for connected STAs */
 	if (si->connected != STA_CONNECTED)
 		return false;
@@ -465,6 +471,11 @@ usteer_local_node_snr_kick(struct usteer_local_node *ln)
 	ev.threshold.ref = min_signal;
 
 	list_for_each_entry(si, &ln->node.sta_info, node_list) {
+		if (usteer_sta_is_policy_exempt(si->sta->addr)) {
+			si->below_min_snr = 0;
+			continue;
+		}
+
 		if (si->connected != STA_CONNECTED)
 			continue;
 
@@ -536,6 +547,9 @@ usteer_local_node_load_kick(struct usteer_local_node *ln)
 	list_for_each_entry(si, &ln->node.sta_info, node_list) {
 		struct sta_info *tmp;
 
+		if (usteer_sta_is_policy_exempt(si->sta->addr))
+			continue;
+
 		if (si->connected != STA_CONNECTED)
 			continue;
 
@@ -582,6 +596,11 @@ usteer_local_node_perform_kick(struct usteer_local_node *ln)
 		if (!si->kick_time || si->kick_time > current_time)
 			continue;
 
+		if (usteer_sta_is_policy_exempt(si->sta->addr)) {
+			si->kick_time = 0;
+			continue;
+		}
+
 		usteer_ubus_kick_client(si);
 	}
 }
diff --git a/sta.c b/sta.c
index ed7e40e..9b53c87 100644
--- a/sta.c
+++ b/sta.c
@@ -17,6 +17,13 @@
  *   Copyright (C) 2020 John Crispin <john at phrozen.org> 
  */
 
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <net/ethernet.h>
+#ifdef linux
+#include <netinet/ether.h>
+#endif
+
 #include "usteer.h"
 
 static int
@@ -28,6 +35,44 @@ avl_macaddr_cmp(const void *k1, const void *k2, void *ptr)
 AVL_TREE(stations, avl_macaddr_cmp, false, NULL);
 static struct usteer_timeout_queue tq;
 
+void config_set_policy_exempt_sta_list(struct blob_attr *data)
+{
+	free(config.policy_exempt_sta_list);
+
+	if (data && blobmsg_len(data) &&
+	    blobmsg_check_attr_list(data, BLOBMSG_TYPE_STRING))
+		config.policy_exempt_sta_list = blob_memdup(data);
+	else
+		config.policy_exempt_sta_list = NULL;
+}
+
+void config_get_policy_exempt_sta_list(struct blob_buf *buf)
+{
+	if (config.policy_exempt_sta_list)
+		blobmsg_add_blob(buf, config.policy_exempt_sta_list);
+}
+
+bool usteer_sta_is_policy_exempt(const uint8_t *addr)
+{
+	struct blob_attr *cur;
+	int rem;
+
+	if (!addr || !config.policy_exempt_sta_list)
+		return false;
+
+	blobmsg_for_each_attr(cur, config.policy_exempt_sta_list, rem) {
+		uint8_t *exempt = (uint8_t *) ether_aton(blobmsg_get_string(cur));
+
+		if (!exempt)
+			continue;
+
+		if (!memcmp(exempt, addr, 6))
+			return true;
+	}
+
+	return false;
+}
+
 static void
 usteer_sta_del(struct sta *sta)
 {
diff --git a/ubus.c b/ubus.c
index 40daf74..285a9ba 100644
--- a/ubus.c
+++ b/ubus.c
@@ -39,6 +39,12 @@ blobmsg_open_table_mac(struct blob_buf *buf, uint8_t *addr)
 	return blobmsg_open_table(buf, str);
 }
 
+static bool
+usteer_ubus_sta_policy_exempt(struct sta_info *si)
+{
+	return si && si->sta && usteer_sta_is_policy_exempt(si->sta->addr);
+}
+
 static int
 usteer_ubus_get_clients(struct ubus_context *ctx, struct ubus_object *obj,
 		       struct ubus_request_data *req, const char *method,
@@ -55,6 +61,8 @@ usteer_ubus_get_clients(struct ubus_context *ctx, struct ubus_object *obj,
 			_cur_n = blobmsg_open_table(&b, usteer_node_name(si->node));
 			blobmsg_add_u8(&b, "connected", si->connected);
 			blobmsg_add_u32(&b, "signal", si->signal);
+			blobmsg_add_u8(&b, "policy_exempt",
+					usteer_sta_is_policy_exempt(sta->addr));
 			blobmsg_close_table(&b, _cur_n);
 		}
 		blobmsg_close_table(&b, _s);
@@ -111,6 +119,8 @@ usteer_ubus_get_client_info(struct ubus_context *ctx, struct ubus_object *obj,
 		_cur_n = blobmsg_open_table(&b, usteer_node_name(si->node));
 		blobmsg_add_u8(&b, "connected", si->connected);
 		blobmsg_add_u32(&b, "signal", si->signal);
+		blobmsg_add_u8(&b, "policy_exempt",
+				usteer_sta_is_policy_exempt(sta->addr));
 		_s = blobmsg_open_table(&b, "stats");
 		for (i = 0; i < __EVENT_TYPE_MAX; i++)
 			usteer_ubus_add_stats(&si->stats[EVENT_TYPE_PROBE], event_types[i]);
@@ -187,7 +197,8 @@ struct cfg_item {
 	_cfg(ARRAY_CB, interfaces), \
 	_cfg(STRING_CB, node_up_script), \
 	_cfg(ARRAY_CB, event_log_types), \
-	_cfg(ARRAY_CB, ssid_list)
+	_cfg(ARRAY_CB, ssid_list), \
+	_cfg(ARRAY_CB, policy_exempt_sta_list)
 
 enum cfg_items {
 #define _cfg(_type, _name) CFG_##_name
@@ -415,6 +426,8 @@ usteer_ubus_get_connected_clients(struct ubus_context *ctx, struct ubus_object *
 
 			s = blobmsg_open_table_mac(&b, si->sta->addr);
 			blobmsg_add_u32(&b, "signal", si->signal);
+			blobmsg_add_u8(&b, "policy_exempt",
+					usteer_sta_is_policy_exempt(si->sta->addr));
 			blobmsg_add_u64(&b, "created", current_time - si->created);
 			blobmsg_add_u64(&b, "connected", current_time - si->connected_since);
 
@@ -674,7 +687,12 @@ int usteer_ubus_bss_transition_request(struct sta_info *si,
 				       uint8_t validity_period,
 				       struct usteer_node *target_node)
 {
-	struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+	struct usteer_local_node *ln;
+
+	if (usteer_ubus_sta_policy_exempt(si))
+		return 0;
+
+	ln = container_of(si->node, struct usteer_local_node, node);
 
 	blob_buf_init(&b, 0);
 	blobmsg_printf(&b, "addr", MAC_ADDR_FMT, MAC_ADDR_DATA(si->sta->addr));
@@ -692,10 +710,15 @@ int usteer_ubus_bss_transition_request(struct sta_info *si,
 
 int usteer_ubus_band_steering_request(struct sta_info *si)
 {
-	struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+	struct usteer_local_node *ln;
 	struct usteer_node *node;
 	void *c;
 
+	if (usteer_ubus_sta_policy_exempt(si))
+		return 0;
+
+	ln = container_of(si->node, struct usteer_local_node, node);
+
 	blob_buf_init(&b, 0);
 	blobmsg_printf(&b, "addr", MAC_ADDR_FMT, MAC_ADDR_DATA(si->sta->addr));
 	blobmsg_add_u32(&b, "dialog_token", 0);
@@ -717,7 +740,12 @@ int usteer_ubus_band_steering_request(struct sta_info *si)
 
 int usteer_ubus_trigger_link_measurement(struct sta_info *si)
 {
-	struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+	struct usteer_local_node *ln;
+
+	if (usteer_ubus_sta_policy_exempt(si))
+		return 0;
+
+	ln = container_of(si->node, struct usteer_local_node, node);
 
 	if (!usteer_sta_supports_link_measurement(si))
 		return 0;
@@ -731,7 +759,12 @@ int usteer_ubus_trigger_link_measurement(struct sta_info *si)
 
 int usteer_ubus_trigger_client_scan(struct sta_info *si)
 {
-	struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+	struct usteer_local_node *ln;
+
+	if (usteer_ubus_sta_policy_exempt(si))
+		return 0;
+
+	ln = container_of(si->node, struct usteer_local_node, node);
 
 	if (!usteer_sta_supports_beacon_measurement_mode(si, BEACON_MEASUREMENT_ACTIVE)) {
 		MSG(DEBUG, "STA does not support beacon measurement sta=" MAC_ADDR_FMT "\n", MAC_ADDR_DATA(si->sta->addr));
@@ -752,7 +785,12 @@ int usteer_ubus_trigger_client_scan(struct sta_info *si)
 
 void usteer_ubus_kick_client(struct sta_info *si)
 {
-	struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+	struct usteer_local_node *ln;
+
+	if (usteer_ubus_sta_policy_exempt(si))
+		return;
+
+	ln = container_of(si->node, struct usteer_local_node, node);
 
 	blob_buf_init(&b, 0);
 	blobmsg_printf(&b, "addr", MAC_ADDR_FMT, MAC_ADDR_DATA(si->sta->addr));
diff --git a/usteer.h b/usteer.h
index f692fb8..8f7736c 100644
--- a/usteer.h
+++ b/usteer.h
@@ -206,6 +206,7 @@ struct usteer_config {
 	uint32_t event_log_mask;
 
 	struct blob_attr *ssid_list;
+	struct blob_attr *policy_exempt_sta_list;
 };
 
 struct usteer_bss_tm_query {
@@ -349,6 +350,7 @@ struct sta_info *usteer_sta_info_get(struct sta *sta, struct usteer_node *node,
 
 bool usteer_sta_supports_beacon_measurement_mode(struct sta_info *si, enum usteer_beacon_measurement_mode mode);
 bool usteer_sta_supports_link_measurement(struct sta_info *si);
+bool usteer_sta_is_policy_exempt(const uint8_t *addr);
 
 void usteer_sta_disconnected(struct sta_info *si);
 void usteer_sta_info_update_timeout(struct sta_info *si, int timeout);
@@ -376,6 +378,9 @@ void config_get_node_up_script(struct blob_buf *buf);
 void config_set_ssid_list(struct blob_attr *data);
 void config_get_ssid_list(struct blob_buf *buf);
 
+void config_set_policy_exempt_sta_list(struct blob_attr *data);
+void config_get_policy_exempt_sta_list(struct blob_buf *buf);
+
 int usteer_interface_init(void);
 void usteer_interface_add(const char *name);
 void usteer_sta_node_cleanup(struct usteer_node *node);
-- 
2.43.0



More information about the openwrt-devel mailing list