[PATCH usteer 5/5] remote: drop stale associations after a roam
Patrick Hein
bagbag98 at googlemail.com
Sun Oct 25 10:38:36 PDT 2026
A station that roams away, e.g. with 802.11r FT over the air, does not
always disassociate from its old AP. The old AP keeps it until it
notices that the station is gone (123 s and 163 s in tests with an
iPhone) and meanwhile forwards intra-BSS traffic from its other clients
to it, which is lost.
Report the association age of each connected station to the other
nodes. A station can only be associated with one AP at a time, so a
node whose association is older than another node's by more than 2.5 s
removes it with hostapd's del_client (disassociation, reason 1, no
ban). The local age is read again first, as the station may have
reassociated since the last poll; if that fails, nothing is dropped.
Unlike a kick, the drop does not update kick_count and roam_kick, as
the station has already left. Factor out the node handlers' station
update and the del_client request for this.
The margin covers the whole-second resolution of the nl80211 connected
time on both nodes, plus 0.5 s for the report to arrive. Associations
whose reported ages differ by 2.5 s or less are left alone. A report
delayed by more than 0.5 s can get the order of two associations wrong
if they started less than the excess apart; the newer one is then
dropped and the station reconnects.
The check runs on every report, so the order in which nodes learn about
a station does not matter. The new message field is optional: older
nodes ignore it, and nothing is dropped because of a node that does not
send it. Associations on other BSSes of the same hostapd are already
removed by hostapd.
Disabled by default (drop_roamed_sta). Drops are logged as the
roamed_sta_drop event.
Signed-off-by: Patrick Hein <bagbag98 at googlemail.com>
---
event.c | 2 +
event.h | 1 +
local_node.c | 20 ++++---
main.c | 2 +
openwrt/usteer/files/etc/config/usteer | 6 +++
openwrt/usteer/files/etc/init.d/usteer | 1 +
parse.c | 4 ++
remote.c | 73 ++++++++++++++++++++++++++
remote.h | 2 +
ubus.c | 20 ++++---
usteer.h | 4 ++
11 files changed, 121 insertions(+), 14 deletions(-)
diff --git a/event.c b/event.c
index 0f8033c..c0ffaa2 100644
--- a/event.c
+++ b/event.c
@@ -35,6 +35,7 @@ static const char * const uev_name[] = {
[UEV_LOAD_KICK_NO_CLIENT] = "load_kick_no_client",
[UEV_LOAD_KICK_CLIENT] = "load_kick_client",
[UEV_SIGNAL_KICK] = "signal_kick",
+ [UEV_ROAMED_STA_DROP] = "roamed_sta_drop",
};
static const char * const uev_reason[] = {
@@ -222,6 +223,7 @@ void config_set_event_log_types(struct blob_attr *attr)
[MSG_INFO] =
(1 << UEV_LOAD_KICK_CLIENT) |
(1 << UEV_SIGNAL_KICK) |
+ (1 << UEV_ROAMED_STA_DROP) |
(1 << UEV_AUTH_REQ_DENY) |
(1 << UEV_ASSOC_REQ_DENY),
[MSG_VERBOSE] =
diff --git a/event.h b/event.h
index 86d9232..056883f 100644
--- a/event.h
+++ b/event.h
@@ -14,6 +14,7 @@ enum uevent_type {
UEV_LOAD_KICK_NO_CLIENT,
UEV_LOAD_KICK_CLIENT,
UEV_SIGNAL_KICK,
+ UEV_ROAMED_STA_DROP,
};
enum uevent_reason {
diff --git a/local_node.c b/local_node.c
index d90e4a5..8ee1505 100644
--- a/local_node.c
+++ b/local_node.c
@@ -440,11 +440,22 @@ usteer_local_node_update_sta_rrm_wnm(struct sta_info *si, struct blob_attr *clie
}
}
+void usteer_local_node_update_sta(struct usteer_node *node, struct sta_info *si)
+{
+ struct usteer_node_handler *h;
+
+ list_for_each_entry(h, &node_handlers, list) {
+ if (!h->update_sta)
+ continue;
+
+ h->update_sta(node, si);
+ }
+}
+
static void
usteer_local_node_set_assoc(struct usteer_local_node *ln, struct blob_attr *cl)
{
struct usteer_node *node = &ln->node;
- struct usteer_node_handler *h;
struct blob_attr *cur;
struct sta_info *si;
struct sta *sta;
@@ -467,12 +478,7 @@ usteer_local_node_set_assoc(struct usteer_local_node *ln, struct blob_attr *cl)
sta = usteer_sta_get(addr, true);
si = usteer_sta_info_get(sta, node, &create);
- list_for_each_entry(h, &node_handlers, list) {
- if (!h->update_sta)
- continue;
-
- h->update_sta(node, si);
- }
+ usteer_local_node_update_sta(node, si);
usteer_local_node_assoc_update(si, cur);
if (si->connected == STA_CONNECTED) {
si->last_connected = current_time;
diff --git a/main.c b/main.c
index bd6ae4b..870059a 100644
--- a/main.c
+++ b/main.c
@@ -118,6 +118,8 @@ void usteer_init_defaults(void)
config.min_snr_kick_delay = 5 * 1000;
+ config.drop_roamed_sta = false;
+
config.load_kick_enabled = false;
config.load_kick_threshold = 75;
config.load_kick_delay = 10 * 1000;
diff --git a/openwrt/usteer/files/etc/config/usteer b/openwrt/usteer/files/etc/config/usteer
index dc06b7c..cdeafb2 100644
--- a/openwrt/usteer/files/etc/config/usteer
+++ b/openwrt/usteer/files/etc/config/usteer
@@ -90,6 +90,11 @@ config usteer
# as a roam
#option roam_process_timeout 5000
+ # Disassociate a station that has associated more recently with another node,
+ # instead of waiting for the AP to notice that it is gone (0/1). Needs nl80211,
+ # and the other nodes must run a usteer with this option (enabled or not).
+ #option drop_roamed_sta 0
+
# Minimum signal-to-noise ratio or signal level (dBm) before attempting to trigger
# client scans for roaming
#option roam_scan_snr 0
@@ -169,6 +174,7 @@ config usteer
# - load_kick_no_client
# - load_kick_client
# - signal_kick
+ # - roamed_sta_drop
#list event_log_types ''
# List of SSIDs to enable steering on
diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer
index a76230b..37d2e75 100755
--- a/openwrt/usteer/files/etc/init.d/usteer
+++ b/openwrt/usteer/files/etc/init.d/usteer
@@ -73,6 +73,7 @@ uci_usteer() {
uci_option_to_json_bool "$cfg" local_mode
uci_option_to_json_bool "$cfg" load_kick_enabled
uci_option_to_json_bool "$cfg" assoc_steering
+ uci_option_to_json_bool "$cfg" drop_roamed_sta
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" aggressiveness_mac_list
diff --git a/parse.c b/parse.c
index 2aa3716..b98bd0a 100644
--- a/parse.c
+++ b/parse.c
@@ -132,6 +132,7 @@ bool parse_apmsg_sta(struct apmsg_sta *msg, struct blob_attr *data)
[APMSG_STA_TIMEOUT] = { .type = BLOB_ATTR_INT32 },
[APMSG_STA_CONNECTED] = { .type = BLOB_ATTR_INT8 },
[APMSG_STA_LAST_CONNECTED] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_STA_CONNECTED_TIME] = { .type = BLOB_ATTR_INT64 },
};
struct blob_attr *tb[__APMSG_STA_MAX];
@@ -153,6 +154,9 @@ bool parse_apmsg_sta(struct apmsg_sta *msg, struct blob_attr *data)
msg->timeout = blob_get_int32(tb[APMSG_STA_TIMEOUT]);
msg->connected = blob_get_int8(tb[APMSG_STA_CONNECTED]);
msg->last_connected = blob_get_int32(tb[APMSG_STA_LAST_CONNECTED]);
+ /* Optional, older versions do not send it */
+ msg->connected_time = tb[APMSG_STA_CONNECTED_TIME] ?
+ blob_get_int64(tb[APMSG_STA_CONNECTED_TIME]) : -1;
return true;
}
diff --git a/remote.c b/remote.c
index b88f2ce..ae0399e 100644
--- a/remote.c
+++ b/remote.c
@@ -33,6 +33,7 @@
#include "usteer.h"
#include "remote.h"
#include "node.h"
+#include "event.h"
static uint32_t local_id;
static struct uloop_fd remote_fd;
@@ -151,6 +152,65 @@ void config_get_interfaces(struct blob_buf *buf)
blobmsg_close_array(buf, c);
}
+/*
+ * nl80211 reports the connected time in whole seconds, so an association
+ * age can be off by up to a second; a report also takes a moment to arrive.
+ */
+#define STALE_ASSOC_MARGIN 2500
+
+static bool
+usteer_sta_assoc_older(struct sta_info *si, struct sta_info *other)
+{
+ if (!si->connected_since)
+ return false;
+
+ return si->connected_since + STALE_ASSOC_MARGIN <
+ other->connected_since;
+}
+
+/*
+ * A station roaming away (e.g. with 802.11r FT) does not always disassociate
+ * from its old AP. It can only be associated with one AP at a time, so the
+ * older of two associations is stale.
+ */
+static void
+usteer_drop_roamed_sta(struct sta_info *remote_si)
+{
+ struct usteer_node *node;
+ struct sta_info *si;
+ uint64_t connected_since;
+
+ for_each_local_node(node) {
+ struct uevent ev = {
+ .type = UEV_ROAMED_STA_DROP,
+ .si_other = remote_si,
+ .node_other = remote_si->node,
+ };
+
+ si = usteer_sta_info_get(remote_si->sta, node, NULL);
+ if (!si || si->connected != STA_CONNECTED ||
+ !usteer_sta_assoc_older(si, remote_si))
+ continue;
+
+ /* The station may have reassociated since the last poll */
+ connected_since = si->connected_since;
+ si->connected_since = 0;
+ usteer_local_node_update_sta(node, si);
+ if (!si->connected_since) {
+ si->connected_since = connected_since;
+ continue;
+ }
+
+ if (!usteer_sta_assoc_older(si, remote_si) ||
+ usteer_ubus_del_client(si, KICK_REASON_UNSPECIFIED, false))
+ continue;
+
+ ev.si_cur = si;
+ usteer_event(&ev);
+ usteer_sta_disconnected(si);
+ }
+}
+
static void
interface_add_station(struct usteer_remote_node *node, struct blob_attr *data)
{
@@ -184,6 +244,16 @@ interface_add_station(struct usteer_remote_node *node, struct blob_attr *data)
si->signal = msg.signal;
si->seen = current_time - msg.seen;
si->last_connected = current_time - msg.last_connected;
+ /* Unknown if not sent, or if older than our uptime */
+ if (msg.connected_time >= 0 &&
+ (uint64_t)msg.connected_time < current_time)
+ si->connected_since = current_time - msg.connected_time;
+ else
+ si->connected_since = 0;
+
+ if (config.drop_roamed_sta && si->connected == STA_CONNECTED &&
+ si->connected_since)
+ usteer_drop_roamed_sta(si);
/* Check if client roamed to this foreign node */
if ((connect_change || create) && si->connected == STA_CONNECTED) {
@@ -558,6 +628,9 @@ static void usteer_send_sta_info(struct sta_info *sta)
blob_put_int32(&buf, APMSG_STA_SEEN, seen);
blob_put_int32(&buf, APMSG_STA_LAST_CONNECTED, last_connected);
blob_put_int32(&buf, APMSG_STA_TIMEOUT, config.local_sta_timeout - seen);
+ if (sta->connected == STA_CONNECTED && sta->connected_since)
+ blob_put_int64(&buf, APMSG_STA_CONNECTED_TIME,
+ current_time - sta->connected_since);
blob_nest_end(&buf, c);
}
diff --git a/remote.h b/remote.h
index a1486e7..18c26c4 100644
--- a/remote.h
+++ b/remote.h
@@ -77,6 +77,7 @@ enum {
APMSG_STA_SEEN,
APMSG_STA_CONNECTED,
APMSG_STA_LAST_CONNECTED,
+ APMSG_STA_CONNECTED_TIME,
__APMSG_STA_MAX
};
@@ -88,6 +89,7 @@ struct apmsg_sta {
int timeout;
int seen;
int last_connected;
+ int64_t connected_time; /* -1: not sent */
};
bool parse_apmsg(struct apmsg *msg, struct blob_attr *data);
diff --git a/ubus.c b/ubus.c
index dca6f9a..db57d34 100644
--- a/ubus.c
+++ b/ubus.c
@@ -179,6 +179,7 @@ struct cfg_item {
_cfg(U32, roam_kick_delay), \
_cfg(U32, signal_diff_threshold), \
_cfg(U32, initial_connect_delay), \
+ _cfg(BOOL, drop_roamed_sta), \
_cfg(BOOL, load_kick_enabled), \
_cfg(U32, load_kick_threshold), \
_cfg(U32, load_kick_delay), \
@@ -777,15 +778,20 @@ int usteer_ubus_trigger_client_scan(struct sta_info *si)
return ubus_invoke(ubus_ctx, ln->obj_id, "rrm_beacon_req", b.head, NULL, 0, 100);
}
+int usteer_ubus_del_client(struct sta_info *si, uint32_t reason_code, bool deauth)
+{
+ struct usteer_local_node *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, "reason", reason_code);
+ blobmsg_add_u8(&b, "deauth", deauth);
+ return ubus_invoke(ubus_ctx, ln->obj_id, "del_client", b.head, NULL, 0, 100);
+}
+
void usteer_ubus_kick_client(struct sta_info *si, uint32_t kick_reason_code)
{
- struct usteer_local_node *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, "reason", kick_reason_code);
- blobmsg_add_u8(&b, "deauth", 1);
- ubus_invoke(ubus_ctx, ln->obj_id, "del_client", b.head, NULL, 0, 100);
+ usteer_ubus_del_client(si, kick_reason_code, true);
usteer_sta_disconnected(si);
si->kick_count++;
si->roam_kick = current_time;
diff --git a/usteer.h b/usteer.h
index 407d02d..c699731 100644
--- a/usteer.h
+++ b/usteer.h
@@ -209,6 +209,8 @@ struct usteer_config {
uint32_t initial_connect_delay;
+ bool drop_roamed_sta;
+
bool load_kick_enabled;
uint32_t load_kick_threshold;
uint32_t load_kick_delay;
@@ -341,6 +343,7 @@ int usteer_snr_to_signal(struct usteer_node *node, int snr);
void usteer_local_nodes_init(struct ubus_context *ctx);
void usteer_local_node_kick(struct usteer_local_node *ln);
+void usteer_local_node_update_sta(struct usteer_node *node, struct sta_info *si);
int usteer_local_node_get_beacon_interval(struct usteer_local_node *ln);
@@ -352,6 +355,7 @@ void usteer_band_steering_sta_update(struct sta_info *si);
bool usteer_band_steering_is_target(struct usteer_local_node *ln, struct usteer_node *node);
void usteer_ubus_init(struct ubus_context *ctx);
+int usteer_ubus_del_client(struct sta_info *si, uint32_t reason_code, bool deauth);
void usteer_ubus_kick_client(struct sta_info *si, uint32_t kick_reason_code);
int usteer_ubus_trigger_client_scan(struct sta_info *si);
int usteer_ubus_band_steering_request(struct sta_info *si,
--
2.56.0
More information about the openwrt-devel
mailing list