From 838ac4d253d9fb77c1225d7fd652a504310914ae Mon Sep 17 00:00:00 2001 From: Grzegorz Kaczor Date: Sat, 25 Jul 2026 11:15:48 +0200 Subject: [PATCH 1/5] Add known-station tracking with cold-start exploratory candidates Persist the best signal ever observed for a station on each node (known.c), synced between usteer peers over the existing remote protocol (remote.c). Stations that never probe or report RRM measurements once associated never generate live sta_info data on any node but their current one, so the normal candidate scan can never find them a roaming target no matter how bad their signal gets - this fallback consults the persisted record instead. Candidates found only through this fallback are exploratory: the signal was never actually observed on that node, just marked "worth exploring" once a peer reports the station connected elsewhere. They are only ever used to trigger a passive BSS-transition-request, never a forced kick or an outright probe/assoc rejection, since the reading backing them could be stale or simply never realized. Adds a known_stations/known_stations_timeout UCI toggle and a delete_known ubus method for manually discarding stale entries. Signed-off-by: Grzegorz Kaczor --- CMakeLists.txt | 2 +- known.c | 266 +++++++++++++++++++++++++ known.h | 79 ++++++++ local_node.c | 3 + main.c | 2 + node.c | 16 ++ openwrt/usteer/files/etc/init.d/usteer | 4 +- parse.c | 25 +++ policy.c | 129 +++++++++++- remote.c | 58 ++++++ remote.h | 16 ++ ubus.c | 52 ++++- usteer.h | 7 + 13 files changed, 648 insertions(+), 11 deletions(-) create mode 100644 known.c create mode 100644 known.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f889659..b1fec8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ IF(NOT HAVE_PCAP_H) MESSAGE(FATAL_ERROR "pcap/pcap.h is not found") ENDIF() -SET(SOURCES main.c local_node.c node.c sta.c policy.c ubus.c remote.c parse.c netifd.c timeout.c event.c measurement.c band_steering.c) +SET(SOURCES main.c local_node.c node.c sta.c policy.c ubus.c remote.c parse.c netifd.c timeout.c event.c measurement.c band_steering.c known.c) IF(NL_CFLAGS) ADD_DEFINITIONS(${NL_CFLAGS}) diff --git a/known.c b/known.c new file mode 100644 index 0000000..0026396 --- /dev/null +++ b/known.c @@ -0,0 +1,266 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#include +#include +#include +#include + +#include "known.h" +#include "node.h" + +#define USTEER_KNOWN_DIR "/etc/usteer/known" +#define USTEER_KNOWN_SAVE_INTERVAL (5 * 60 * 1000) + +static struct uloop_timeout save_timer; + +static void +known_path(char *buf, size_t len, const char *name) +{ + int i, n; + + n = snprintf(buf, len, USTEER_KNOWN_DIR "/"); + for (i = 0; name[i] && n < (int) len - 1; i++, n++) + buf[n] = isalnum((unsigned char) name[i]) ? name[i] : '_'; + buf[n] = 0; +} + +void +usteer_known_node_init(struct usteer_node *node, const char *name) +{ + char path[192]; + unsigned int addr[6]; + int signal; + unsigned long long ts; + FILE *f; + + INIT_LIST_HEAD(&node->known_sta); + node->known_name = NULL; + + /* Remote nodes (name == NULL) are never persisted to disk - their + * known-station data only ever arrives over the wire, and only + * stays around for as long as that peer keeps broadcasting. + * + * known_stations can be enabled per-SSID, but the SSID isn't known + * yet this early in node bring-up, so it isn't checked here - + * loading a leftover file for a node whose SSID turns out to have + * the feature disabled is harmless (it will simply never be + * consulted; see find_known_candidate() in policy.c). */ + if (!name) + return; + + node->known_name = strdup(name); + + known_path(path, sizeof(path), name); + f = fopen(path, "r"); + if (!f) + return; + + while (fscanf(f, "%2x:%2x:%2x:%2x:%2x:%2x %d %llu\n", + &addr[0], &addr[1], &addr[2], &addr[3], &addr[4], &addr[5], + &signal, &ts) == 8) { + struct usteer_known_sta *ks; + uint8_t mac[6]; + int i; + + for (i = 0; i < 6; i++) + mac[i] = addr[i]; + + ks = calloc(1, sizeof(*ks)); + memcpy(ks->addr, mac, sizeof(ks->addr)); + ks->signal = signal; + ks->timestamp = ts; + list_add(&ks->list, &node->known_sta); + } + + fclose(f); + usteer_known_prune(node); + + MSG(INFO, "Loaded %s\n", path); +} + +void +usteer_known_node_free(struct usteer_node *node) +{ + struct usteer_known_sta *ks, *tmp; + + list_for_each_entry_safe(ks, tmp, &node->known_sta, list) { + list_del(&ks->list); + free(ks); + } + + free(node->known_name); + node->known_name = NULL; +} + +struct usteer_known_sta * +usteer_known_find(struct usteer_node *node, const uint8_t *addr) +{ + struct usteer_known_sta *ks; + + list_for_each_entry(ks, &node->known_sta, list) { + if (!memcmp(ks->addr, addr, 6)) + return ks; + } + + return NULL; +} + +void +usteer_known_delete(struct usteer_node *node, const uint8_t *addr) +{ + struct usteer_known_sta *ks = usteer_known_find(node, addr); + + if (!ks) + return; + + list_del(&ks->list); + free(ks); +} + +/* + * Most recent known-signal timestamp for addr across every node this + * process currently has visibility into: our own local nodes, plus any + * remote node that is presently, actively broadcasting. A station that + * is confirmed alive anywhere in the network keeps ALL of its recorded + * entries alive - including on nodes it hasn't personally been near in + * a while - because those are exactly the "known good elsewhere" + * candidates the whole feature exists to preserve. Only a station that + * nobody has seen anywhere for known_stations_timeout is actually gone. + */ +static uint64_t +known_last_seen_anywhere(const uint8_t *addr) +{ + struct usteer_remote_node *rn; + struct usteer_node *node; + struct usteer_known_sta *ks; + uint64_t last = 0; + + for_each_local_node(node) { + ks = usteer_known_find(node, addr); + if (ks && ks->timestamp > last) + last = ks->timestamp; + } + + for_each_remote_node(rn) { + ks = usteer_known_find(&rn->node, addr); + if (ks && ks->timestamp > last) + last = ks->timestamp; + } + + return last; +} + +void +usteer_known_prune(struct usteer_node *node) +{ + struct usteer_known_sta *ks, *tmp; + uint32_t timeout = config.known_stations_timeout; + uint64_t max_age, last_seen; + + if (!timeout) + return; + + max_age = (uint64_t) timeout * 1000; + list_for_each_entry_safe(ks, tmp, &node->known_sta, list) { + last_seen = known_last_seen_anywhere(ks->addr); + if (current_time - last_seen <= max_age) + continue; + + list_del(&ks->list); + free(ks); + } +} + +void +usteer_known_update(struct usteer_node *node, const uint8_t *addr, int signal) +{ + struct usteer_known_sta *ks; + + if (!config.known_stations) + return; + + ks = usteer_known_find(node, addr); + if (!ks) { + ks = calloc(1, sizeof(*ks)); + memcpy(ks->addr, addr, sizeof(ks->addr)); + ks->signal = signal; + ks->timestamp = current_time; + list_add(&ks->list, &node->known_sta); + return; + } + + /* signal == 0 is the "unknown, worth exploring" placeholder (see + * remote.c, interface_add_station()) - a real reading is always + * negative dBm in practice, so it unconditionally supersedes a + * placeholder. Otherwise refresh on any reading at least as good + * as the stored one, so a station that stays put doesn't age out + * even though its signal never improves further. */ + if (ks->signal == 0 || signal >= ks->signal) { + ks->signal = signal; + ks->timestamp = current_time; + } +} + +static void +usteer_known_node_save(struct usteer_node *node) +{ + struct usteer_known_sta *ks; + char path[192]; + FILE *f; + + if (!node->known_name || list_empty(&node->known_sta)) + return; + + known_path(path, sizeof(path), node->known_name); + f = fopen(path, "w"); + if (!f) + return; + + list_for_each_entry(ks, &node->known_sta, list) { + fprintf(f, MAC_ADDR_FMT " %d %llu\n", + MAC_ADDR_DATA(ks->addr), ks->signal, + (unsigned long long) ks->timestamp); + } + + fclose(f); +} + +static void +usteer_known_save_timer(struct uloop_timeout *t) +{ + struct usteer_node *node; + + uloop_timeout_set(t, USTEER_KNOWN_SAVE_INTERVAL); + + for_each_local_node(node) { + if (!config.known_stations) + continue; + + usteer_known_prune(node); + usteer_known_node_save(node); + } +} + +void +usteer_known_init(void) +{ + /* plain mkdir() does not create intermediate directories */ + mkdir("/etc/usteer", 0755); + mkdir(USTEER_KNOWN_DIR, 0755); + + save_timer.cb = usteer_known_save_timer; + uloop_timeout_set(&save_timer, USTEER_KNOWN_SAVE_INTERVAL); +} diff --git a/known.h b/known.h new file mode 100644 index 0000000..1f32439 --- /dev/null +++ b/known.h @@ -0,0 +1,79 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __USTEER_KNOWN_H +#define __USTEER_KNOWN_H + +#include "usteer.h" + +/* + * Optional, opt-in (config.known_stations) persisted memory of the best + * signal ever locally observed for a station on a given node. + * + * This exists because some stations (cheap/embedded WiFi stacks: home + * appliances, IoT sensors, ...) never probe or report RRM neighbor/beacon + * measurements once associated, so usteer's normal candidate search + * (which only ever compares stations it has *live* signal data for on + * another node) can never find a better node for them, no matter how bad + * their current signal gets. + * + * For a stationary station this historical reading remains physically + * valid indefinitely, so it is kept on disk and re-loaded on restart. + * A remote node's known-station data only ever exists for as long as + * that node is actively broadcasting on the usteer remote protocol (see + * remote.c) - a peer that stops sending updates has its remote node (and + * with it, its known-station list) removed by the existing + * remote_node_timeout logic, so a station can never be pointed at a peer + * that is not confirmed alive in the current update cycle. + * + * Cold start: whenever a node learns, via the normal remote exchange, + * that some station is connected to a peer (interface_add_station() in + * remote.c), it also seeds a signal == 0 placeholder for that station on + * each of its own local nodes that share its SSID and don't already have + * any entry. 0 is never a real reading (real dBm signal is always + * negative), so it always looks like a large improvement to the normal + * comparison logic - the peer that actually holds the connection ends up + * offering this node as a candidate and pushing the station over via the + * ordinary roam path. usteer_known_update() then unconditionally + * replaces the placeholder with whatever real signal is measured there, + * after which the station is judged on its merits like any other - sent + * straight back if the untested node turns out to be worse. + */ +struct usteer_known_sta { + struct list_head list; + + uint8_t addr[6]; + int signal; + uint64_t timestamp; /* current_time (ms) of this reading */ +}; + +void usteer_known_node_init(struct usteer_node *node, const char *name); +void usteer_known_node_free(struct usteer_node *node); + +/* Record a fresh, locally-observed signal reading (keep-if-better). */ +void usteer_known_update(struct usteer_node *node, const uint8_t *addr, int signal); + +/* Find the best known-signal record for addr on the given node, if any. */ +struct usteer_known_sta *usteer_known_find(struct usteer_node *node, const uint8_t *addr); + +/* Manually remove a single record, e.g. on user request from the UI. */ +void usteer_known_delete(struct usteer_node *node, const uint8_t *addr); + +/* Drop entries older than config.known_stations_timeout (0 = never). */ +void usteer_known_prune(struct usteer_node *node); + +void usteer_known_init(void); + +#endif diff --git a/local_node.c b/local_node.c index d90e4a5..1227115 100644 --- a/local_node.c +++ b/local_node.c @@ -30,6 +30,7 @@ #include #include "usteer.h" #include "node.h" +#include "known.h" AVL_TREE(local_nodes, avl_strcmp, false, NULL); static struct blob_buf b; @@ -72,6 +73,7 @@ usteer_free_node(struct ubus_context *ctx, struct usteer_local_node *ln) usteer_local_node_state_reset(ln); usteer_sta_node_cleanup(&ln->node); usteer_measurement_report_node_cleanup(&ln->node); + usteer_known_node_free(&ln->node); uloop_timeout_cancel(&ln->update); uloop_timeout_cancel(&ln->bss_tm_queries_timeout); avl_delete(&local_nodes, &ln->node.avl); @@ -783,6 +785,7 @@ usteer_get_node(struct ubus_context *ctx, const char *name) kvlist_init(&ln->node_info, kvlist_blob_len); INIT_LIST_HEAD(&node->sta_info); INIT_LIST_HEAD(&node->measurements); + usteer_known_node_init(node, name); ln->bss_tm_queries_timeout.cb = usteer_local_node_process_bss_tm_queries; INIT_LIST_HEAD(&ln->bss_tm_queries); diff --git a/main.c b/main.c index bd6ae4b..af96f66 100644 --- a/main.c +++ b/main.c @@ -26,6 +26,7 @@ #include "usteer.h" #include "event.h" #include "node.h" +#include "known.h" struct ubus_context *ubus_ctx; struct usteer_config config = {}; @@ -228,6 +229,7 @@ int main(int argc, char **argv) } else { usteer_ubus_init(ubus_ctx); usteer_local_nodes_init(ubus_ctx); + usteer_known_init(); } uloop_run(); diff --git a/node.c b/node.c index 02dd30a..14785f1 100644 --- a/node.c +++ b/node.c @@ -46,6 +46,22 @@ struct usteer_node *usteer_node_by_bssid(uint8_t *bssid) { return NULL; } +struct usteer_node *usteer_node_by_name(const char *name) { + struct usteer_local_node *ln; + struct usteer_remote_node *rn; + + ln = avl_find_element(&local_nodes, name, ln, node.avl); + if (ln) + return &ln->node; + + for_each_remote_node(rn) { + if (!strcmp(usteer_node_name(&rn->node), name)) + return &rn->node; + } + + return NULL; +} + void usteer_node_set_blob(struct blob_attr **dest, struct blob_attr *val) { int new_len; diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer index 5c99bec..da9ab65 100755 --- a/openwrt/usteer/files/etc/init.d/usteer +++ b/openwrt/usteer/files/etc/init.d/usteer @@ -69,6 +69,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" known_stations 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 @@ -88,7 +89,8 @@ uci_usteer() { link_measurement_interval \ band_steering_interval band_steering_min_snr band_steering_signal_threshold \ load_kick_threshold load_kick_delay load_kick_min_clients \ - load_kick_reason_code + load_kick_reason_code \ + known_stations_timeout do uci_option_to_json "$cfg" "$opt" done diff --git a/parse.c b/parse.c index 2aa3716..93c6128 100644 --- a/parse.c +++ b/parse.c @@ -66,6 +66,7 @@ bool parse_apmsg_node(struct apmsg_node *msg, struct blob_attr *data) [APMSG_NODE_NODE_INFO] = { .type = BLOB_ATTR_NESTED }, [APMSG_NODE_CHANNEL] = { .type = BLOB_ATTR_INT32 }, [APMSG_NODE_OP_CLASS] = { .type = BLOB_ATTR_INT32 }, + [APMSG_NODE_KNOWN_STA] = { .type = BLOB_ATTR_NESTED }, }; struct blob_attr *tb[__APMSG_NODE_MAX]; struct blob_attr *cur; @@ -119,6 +120,7 @@ bool parse_apmsg_node(struct apmsg_node *msg, struct blob_attr *data) } msg->node_info = tb[APMSG_NODE_NODE_INFO]; + msg->known_sta = tb[APMSG_NODE_KNOWN_STA]; return true; } @@ -156,3 +158,26 @@ bool parse_apmsg_sta(struct apmsg_sta *msg, struct blob_attr *data) return true; } + +bool parse_apmsg_known_sta(struct apmsg_known_sta *msg, struct blob_attr *data) +{ + static const struct blob_attr_info policy[__APMSG_KNOWN_MAX] = { + [APMSG_KNOWN_ADDR] = { .type = BLOB_ATTR_BINARY }, + [APMSG_KNOWN_SIGNAL] = { .type = BLOB_ATTR_INT32 }, + [APMSG_KNOWN_AGE] = { .type = BLOB_ATTR_INT32 }, + }; + struct blob_attr *tb[__APMSG_KNOWN_MAX]; + + blob_parse(data, tb, policy, __APMSG_KNOWN_MAX); + if (!tb[APMSG_KNOWN_ADDR] || !tb[APMSG_KNOWN_SIGNAL] || !tb[APMSG_KNOWN_AGE]) + return false; + + if (blob_len(tb[APMSG_KNOWN_ADDR]) != sizeof(msg->addr)) + return false; + + memcpy(msg->addr, blob_data(tb[APMSG_KNOWN_ADDR]), sizeof(msg->addr)); + msg->signal = blob_get_int32(tb[APMSG_KNOWN_SIGNAL]); + msg->age = blob_get_int32(tb[APMSG_KNOWN_AGE]); + + return true; +} diff --git a/policy.c b/policy.c index 16c4363..b21166c 100644 --- a/policy.c +++ b/policy.c @@ -20,6 +20,7 @@ #include "usteer.h" #include "node.h" #include "event.h" +#include "known.h" static bool below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new) @@ -114,13 +115,103 @@ is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new) return reasons; } +/* + * Optional fallback (known_stations): stations that never probe or + * report RRM measurements once associated never generate live sta_info + * data on any node but their current one, so the scan in + * find_better_candidate() below can never find them a candidate no + * matter how bad their signal gets. Consult the persisted + * best-signal-ever-seen record instead. + * + * A node only ever has known-station data if it is a local node, or a + * remote node that is currently, actively broadcasting on the usteer + * remote protocol (see remote.c) - a peer that stops sending updates has + * its remote node, and with it its known-station list, removed by the + * existing remote_node_timeout logic. So this can never point a station + * towards a peer that isn't confirmed alive right now. + */ static struct sta_info * -find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t required_criteria, uint64_t max_age) +find_known_candidate(struct sta_info *si_ref, struct uevent *ev) +{ + struct sta_info *si, *candidate = NULL; + struct usteer_remote_node *rn; + struct usteer_node *node; + struct usteer_known_sta *ks; + uint32_t reasons; + bool create; + + if (!config.known_stations) + return NULL; + + for_each_local_node(node) { + if (node == si_ref->node || + strcmp(node->ssid, si_ref->node->ssid) != 0) + continue; + + ks = usteer_known_find(node, si_ref->sta->addr); + if (!ks) + continue; + + si = usteer_sta_info_get(si_ref->sta, node, &create); + if (!si) + continue; + si->signal = ks->signal; + + reasons = is_better_candidate(si_ref, si); + if (!(reasons & (1 << UEV_SELECT_REASON_SIGNAL))) + continue; + + if (candidate && si->signal <= candidate->signal) + continue; + + candidate = si; + if (ev) { + ev->si_other = si; + ev->select_reasons = reasons; + } + } + + for_each_remote_node(rn) { + node = &rn->node; + if (strcmp(node->ssid, si_ref->node->ssid) != 0) + continue; + + ks = usteer_known_find(node, si_ref->sta->addr); + if (!ks) + continue; + + si = usteer_sta_info_get(si_ref->sta, node, &create); + if (!si) + continue; + si->signal = ks->signal; + + reasons = is_better_candidate(si_ref, si); + if (!(reasons & (1 << UEV_SELECT_REASON_SIGNAL))) + continue; + + if (candidate && si->signal <= candidate->signal) + continue; + + candidate = si; + if (ev) { + ev->si_other = si; + ev->select_reasons = reasons; + } + } + + return candidate; +} + +static struct sta_info * +find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t required_criteria, uint64_t max_age, bool *exploratory) { struct sta_info *si, *candidate = NULL; struct sta *sta = si_ref->sta; uint32_t reasons; + if (exploratory) + *exploratory = false; + list_for_each_entry(si, &sta->nodes, list) { if (si == si_ref) continue; @@ -150,6 +241,12 @@ find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t requi candidate = si; } + if (!candidate && (required_criteria & (1 << UEV_SELECT_REASON_SIGNAL))) { + candidate = find_known_candidate(si_ref, ev); + if (candidate && exploratory) + *exploratory = true; + } + return candidate; } @@ -175,6 +272,7 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) }; int min_signal; bool ret = true; + bool exploratory; if (type == EVENT_TYPE_PROBE && !config.probe_steering) goto out; @@ -216,7 +314,14 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) goto out; } - if (!find_better_candidate(si, &ev, UEV_SELECT_REASON_ALL, 0)) + /* A candidate found only via the known-devices fallback (signal + * never actually observed on that node, just a "worth exploring" + * placeholder - see find_known_candidate()) must never be grounds + * to reject a probe/assoc request outright: this station may have + * no better option right now, and rejecting on a pure guess risks + * an assoc-reject loop instead of just a missed steering + * opportunity. */ + if (!find_better_candidate(si, &ev, UEV_SELECT_REASON_ALL, 0, &exploratory) || exploratory) goto out; ev.reason = UEV_REASON_BETTER_CANDIDATE; @@ -303,7 +408,8 @@ usteer_roam_sm_start_scan(struct sta_info *si, struct uevent *ev) } static struct sta_info * -usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum roam_trigger_state next_state) +usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum roam_trigger_state next_state, + bool *exploratory) { uint64_t max_age = 2 * config.roam_scan_interval; struct sta_info *candidate; @@ -311,7 +417,7 @@ usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum ro if (max_age > current_time - si->roam_scan_start) max_age = current_time - si->roam_scan_start; - candidate = find_better_candidate(si, ev, (1 << UEV_SELECT_REASON_SIGNAL), max_age); + candidate = find_better_candidate(si, ev, (1 << UEV_SELECT_REASON_SIGNAL), max_age, exploratory); if (candidate) usteer_roam_set_state(si, next_state, ev); @@ -324,6 +430,7 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) struct sta_info *candidate; uint32_t disassoc_timer; uint32_t validity_period; + bool exploratory = false; struct uevent ev = { .si_cur = si, }; @@ -335,7 +442,7 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) } /* Check if we've found a better node regardless of the scan-interval */ - if (usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE)) + if (usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE, NULL)) break; /* Only scan every scan-interval */ @@ -371,7 +478,7 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) break; } - candidate = usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE); + candidate = usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE, &exploratory); /* Kick back in case no better node is found */ if (!candidate) { usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev); @@ -385,7 +492,13 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) si->roam_transition_start = current_time; si->roam_transition_request_validity_end = current_time + 10000; validity_period = 10000 / usteer_local_node_get_beacon_interval(ln); /* ~ 10 seconds */ - if (si->sta->aggressiveness >= 2) { + /* A candidate from the known-devices fallback (see + * find_known_candidate()) is a guess, not a confirmed better + * signal: the client is free to ignore it, and it must never + * be force-kicked - that would disconnect a station that may + * still be perfectly happy where it is, on the strength of a + * placeholder reading that was never actually observed. */ + if (si->sta->aggressiveness >= 2 && !exploratory) { if (!si->kick_time) si->kick_time = current_time + config.roam_kick_delay; if (si->sta->aggressiveness >= 3) @@ -583,7 +696,7 @@ usteer_local_node_load_kick(struct usteer_local_node *ln) if (is_more_kickable(kick1, si)) kick1 = si; - tmp = find_better_candidate(si, NULL, (1 << UEV_SELECT_REASON_LOAD), 0); + tmp = find_better_candidate(si, NULL, (1 << UEV_SELECT_REASON_LOAD), 0, NULL); if (!tmp) continue; diff --git a/remote.c b/remote.c index bf58ea3..7beadb0 100644 --- a/remote.c +++ b/remote.c @@ -33,6 +33,7 @@ #include "usteer.h" #include "remote.h" #include "node.h" +#include "known.h" static uint32_t local_id; static struct uloop_fd remote_fd; @@ -200,6 +201,24 @@ interface_add_station(struct usteer_remote_node *node, struct blob_attr *data) } usteer_sta_info_update_timeout(si, msg.timeout); + + /* Cold start: we now know this station exists and is connected + * somewhere in the network. Seed a "worth exploring" placeholder + * on our own same-SSID local nodes that don't have any data for + * it yet, so the peer that actually holds the connection sees a + * candidate here and tries pushing it over (see known.h). */ + if (si->connected == STA_CONNECTED) { + for_each_local_node(local_node) { + if (strcmp(local_node->ssid, node->node.ssid) != 0) + continue; + if (!config.known_stations) + continue; + if (usteer_known_find(local_node, msg.addr)) + continue; + + usteer_known_update(local_node, msg.addr, 0); + } + } } static void @@ -211,6 +230,7 @@ remote_node_free(struct usteer_remote_node *node) list_del(&node->host_list); usteer_sta_node_cleanup(&node->node); usteer_measurement_report_node_cleanup(&node->node); + usteer_known_node_free(&node->node); free(node); if (!list_empty(&host->nodes)) @@ -266,6 +286,7 @@ interface_get_node(struct usteer_remote_host *host, const char *name) node->host = host; INIT_LIST_HEAD(&node->node.sta_info); INIT_LIST_HEAD(&node->node.measurements); + usteer_known_node_init(&node->node, NULL); list_add_tail(&node->list, &remote_nodes); list_add_tail(&node->host_list, &host->nodes); @@ -304,6 +325,26 @@ interface_add_node(struct usteer_remote_host *host, struct blob_attr *data) blob_for_each_attr(cur, msg.stations, rem) interface_add_station(node, cur); + + if (config.known_stations && msg.known_sta) { + struct apmsg_known_sta kmsg; + + blob_for_each_attr(cur, msg.known_sta, rem) { + struct usteer_known_sta *ks; + + if (!parse_apmsg_known_sta(&kmsg, cur)) + continue; + + ks = usteer_known_find(&node->node, kmsg.addr); + if (!ks) { + ks = calloc(1, sizeof(*ks)); + memcpy(ks->addr, kmsg.addr, sizeof(ks->addr)); + list_add(&ks->list, &node->node.known_sta); + } + ks->signal = kmsg.signal; + ks->timestamp = current_time - kmsg.age; + } + } } static void @@ -599,6 +640,23 @@ static void usteer_send_node(struct usteer_node *node, struct sta_info *sta) blob_nest_end(&buf, s); + if (config.known_stations && !list_empty(&node->known_sta)) { + struct usteer_known_sta *ks; + void *k, *e; + int age; + + k = blob_nest_start(&buf, APMSG_NODE_KNOWN_STA); + list_for_each_entry(ks, &node->known_sta, list) { + age = current_time - ks->timestamp; + e = blob_nest_start(&buf, 0); + blob_put(&buf, APMSG_KNOWN_ADDR, ks->addr, sizeof(ks->addr)); + blob_put_int32(&buf, APMSG_KNOWN_SIGNAL, ks->signal); + blob_put_int32(&buf, APMSG_KNOWN_AGE, age); + blob_nest_end(&buf, e); + } + blob_nest_end(&buf, k); + } + blob_nest_end(&buf, c); } diff --git a/remote.h b/remote.h index a1486e7..2b8a64f 100644 --- a/remote.h +++ b/remote.h @@ -51,6 +51,7 @@ enum { APMSG_NODE_BSSID, APMSG_NODE_CHANNEL, APMSG_NODE_OP_CLASS, + APMSG_NODE_KNOWN_STA, __APMSG_NODE_MAX }; @@ -68,6 +69,7 @@ struct apmsg_node { struct blob_attr *stations; struct blob_attr *rrm_nr; struct blob_attr *node_info; + struct blob_attr *known_sta; }; enum { @@ -90,8 +92,22 @@ struct apmsg_sta { int last_connected; }; +enum { + APMSG_KNOWN_ADDR, + APMSG_KNOWN_SIGNAL, + APMSG_KNOWN_AGE, + __APMSG_KNOWN_MAX +}; + +struct apmsg_known_sta { + uint8_t addr[6]; + int signal; + int age; +}; + bool parse_apmsg(struct apmsg *msg, struct blob_attr *data); bool parse_apmsg_node(struct apmsg_node *msg, struct blob_attr *data); bool parse_apmsg_sta(struct apmsg_sta *msg, struct blob_attr *data); +bool parse_apmsg_known_sta(struct apmsg_known_sta *msg, struct blob_attr *data); #endif diff --git a/ubus.c b/ubus.c index dca6f9a..d57922f 100644 --- a/ubus.c +++ b/ubus.c @@ -27,6 +27,7 @@ #include "usteer.h" #include "node.h" #include "event.h" +#include "known.h" static struct blob_buf b; static KVLIST(host_info, kvlist_blob_len); @@ -67,6 +68,11 @@ static struct blobmsg_policy client_arg[] = { { .name = "address", .type = BLOBMSG_TYPE_STRING, }, }; +static struct blobmsg_policy known_arg[] = { + { .name = "node", .type = BLOBMSG_TYPE_STRING, }, + { .name = "address", .type = BLOBMSG_TYPE_STRING, }, +}; + static void usteer_ubus_add_stats(struct sta_info_stats *stats, const char *name) { @@ -79,6 +85,32 @@ usteer_ubus_add_stats(struct sta_info_stats *stats, const char *name) blobmsg_close_table(&b, s); } +static int +usteer_ubus_delete_known(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[2]; + struct usteer_node *node; + uint8_t *mac; + + blobmsg_parse(known_arg, 2, tb, blob_data(msg), blob_len(msg)); + if (!tb[0] || !tb[1]) + return UBUS_STATUS_INVALID_ARGUMENT; + + node = usteer_node_by_name(blobmsg_data(tb[0])); + if (!node) + return UBUS_STATUS_NOT_FOUND; + + mac = (uint8_t *) ether_aton(blobmsg_data(tb[1])); + if (!mac) + return UBUS_STATUS_INVALID_ARGUMENT; + + usteer_known_delete(node, mac); + + return 0; +} + static int usteer_ubus_get_client_info(struct ubus_context *ctx, struct ubus_object *obj, struct ubus_request_data *req, const char *method, @@ -190,7 +222,9 @@ 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(BOOL, known_stations), \ + _cfg(U32, known_stations_timeout) enum cfg_items { #define _cfg(_type, _name) CFG_##_name @@ -317,6 +351,21 @@ void usteer_dump_node(struct blob_buf *buf, struct usteer_node *node) blob_data(node->node_info), blob_len(node->node_info)); + if (config.known_stations && !list_empty(&node->known_sta)) { + struct usteer_known_sta *ks; + void *k, *e; + + k = blobmsg_open_array(buf, "known_sta"); + list_for_each_entry(ks, &node->known_sta, list) { + e = blobmsg_open_table(buf, ""); + blobmsg_printf(buf, "address", MAC_ADDR_FMT, MAC_ADDR_DATA(ks->addr)); + blobmsg_add_u32(buf, "signal", ks->signal); + blobmsg_add_u32(buf, "age", current_time - ks->timestamp); + blobmsg_close_table(buf, e); + } + blobmsg_close_array(buf, k); + } + blobmsg_close_table(buf, c); } @@ -580,6 +629,7 @@ static const struct ubus_method usteer_methods[] = { UBUS_METHOD_NOARG("connected_clients", usteer_ubus_get_connected_clients), UBUS_METHOD_NOARG("get_clients", usteer_ubus_get_clients), UBUS_METHOD("get_client_info", usteer_ubus_get_client_info, client_arg), + UBUS_METHOD("delete_known", usteer_ubus_delete_known, known_arg), UBUS_METHOD_NOARG("get_config", usteer_ubus_get_config), UBUS_METHOD("set_config", usteer_ubus_set_config, config_policy), UBUS_METHOD("update_config", usteer_ubus_set_config, config_policy), diff --git a/usteer.h b/usteer.h index 407d02d..4cb0bef 100644 --- a/usteer.h +++ b/usteer.h @@ -89,6 +89,9 @@ struct usteer_node { char ssid[33]; uint8_t bssid[6]; + struct list_head known_sta; + char *known_name; + bool disabled; int freq; int channel; @@ -219,6 +222,9 @@ struct usteer_config { uint32_t event_log_mask; struct blob_attr *ssid_list; + + bool known_stations; + uint32_t known_stations_timeout; }; struct usteer_bss_tm_query { @@ -387,6 +393,7 @@ void usteer_node_set_blob(struct blob_attr **dest, struct blob_attr *val); struct usteer_local_node *usteer_local_node_by_bssid(uint8_t *bssid); struct usteer_remote_node *usteer_remote_node_by_bssid(uint8_t *bssid); struct usteer_node *usteer_node_by_bssid(uint8_t *bssid); +struct usteer_node *usteer_node_by_name(const char *name); struct usteer_node *usteer_node_get_next_neighbor(struct usteer_node *current_node, struct usteer_node *last); bool usteer_check_request(struct sta_info *si, enum usteer_event_type type); From 8fd3526a442c9864ec28028bdbe29a91ff72e632 Mon Sep 17 00:00:00 2001 From: Grzegorz Kaczor Date: Sat, 25 Jul 2026 11:15:48 +0200 Subject: [PATCH 2/5] Add per-SSID configuration overrides Add a usteer_ssid UCI section type keyed by SSID that overrides selected global settings (signal thresholds, roam-scan/trigger tuning, band-steering, load-kick, aggressiveness and its MAC-list) for stations on that SSID only, via the new SSID_CFG(ssid, field) lookup (ssid_config.c). Any option left unset on a usteer_ssid section falls back to the corresponding global usteer setting, so existing single-SSID configs keep working unchanged. Also moves per-station aggressiveness from struct sta (global to the station) to struct sta_info (per station+node), since a station seen on multiple SSIDs must use each SSID's own aggressiveness for decisions made on that SSID's nodes. Signed-off-by: Grzegorz Kaczor --- CMakeLists.txt | 2 +- band_steering.c | 18 ++- local_node.c | 2 +- openwrt/usteer/files/etc/init.d/usteer | 34 ++++- policy.c | 100 +++++++------ ssid_config.c | 197 +++++++++++++++++++++++++ ssid_config.h | 84 +++++++++++ sta.c | 32 +++- ubus.c | 4 +- usteer.h | 8 +- 10 files changed, 418 insertions(+), 63 deletions(-) create mode 100644 ssid_config.c create mode 100644 ssid_config.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b1fec8c..6e84e81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ IF(NOT HAVE_PCAP_H) MESSAGE(FATAL_ERROR "pcap/pcap.h is not found") ENDIF() -SET(SOURCES main.c local_node.c node.c sta.c policy.c ubus.c remote.c parse.c netifd.c timeout.c event.c measurement.c band_steering.c known.c) +SET(SOURCES main.c local_node.c node.c sta.c policy.c ubus.c remote.c parse.c netifd.c timeout.c event.c measurement.c band_steering.c known.c ssid_config.c) IF(NL_CFLAGS) ADD_DEFINITIONS(${NL_CFLAGS}) diff --git a/band_steering.c b/band_steering.c index 9f28603..8eeaf5d 100644 --- a/band_steering.c +++ b/band_steering.c @@ -17,16 +17,19 @@ #include "usteer.h" #include "node.h" +#include "ssid_config.h" void usteer_band_steering_sta_update(struct sta_info *si) { + uint32_t signal_threshold = SSID_CFG(si->node->ssid, band_steering_signal_threshold); + if (si->connected == STA_NOT_CONNECTED) { if (si->band_steering.signal_threshold != NO_SIGNAL) { si->band_steering.signal_threshold = NO_SIGNAL; } return; } - if (config.band_steering_signal_threshold) + if (signal_threshold) { if (si->connected != STA_NOT_CONNECTED && si->band_steering.signal_threshold == NO_SIGNAL) { @@ -40,10 +43,10 @@ void usteer_band_steering_sta_update(struct sta_info *si) si->band_steering.signal_threshold--; MSG(DEBUG, "band steering station " MAC_ADDR_FMT " (%s) reduce threshold %d, signal: %d\n", MAC_ADDR_DATA(si->sta->addr), usteer_node_name(si->node), si->band_steering.signal_threshold, si->signal); } - if (si->signal < si->band_steering.signal_threshold + config.band_steering_signal_threshold) + if (si->signal < si->band_steering.signal_threshold + signal_threshold) si->band_steering.below_snr = true; } - if (si->signal < usteer_snr_to_signal(si->node, config.band_steering_min_snr)) + if (si->signal < usteer_snr_to_signal(si->node, SSID_CFG(si->node->ssid, band_steering_min_snr))) si->band_steering.below_snr = true; } @@ -81,12 +84,13 @@ static bool usteer_band_steering_has_target_iface(struct usteer_local_node *ln) void usteer_band_steering_perform_steer(struct usteer_local_node *ln) { - unsigned int min_count = DIV_ROUND_UP(config.band_steering_interval, config.local_sta_update); + uint32_t band_steering_interval = SSID_CFG(ln->node.ssid, band_steering_interval); + unsigned int min_count = DIV_ROUND_UP(band_steering_interval, config.local_sta_update); struct sta_info *si; uint32_t disassoc_timer; uint32_t validity_period; - if (!config.band_steering_interval) + if (!band_steering_interval) return; /* Band-Steering is only available on 2.4 GHz interfaces */ @@ -123,10 +127,10 @@ void usteer_band_steering_perform_steer(struct usteer_local_node *ln) if (si->bss_transition) { si->roam_transition_request_validity_end = current_time + 10000; validity_period = 10000 / usteer_local_node_get_beacon_interval(ln); /* ~ 10 seconds */ - if (si->sta->aggressiveness >= 2) { + if (si->aggressiveness >= 2) { if (!si->kick_time) si->kick_time = current_time + config.roam_kick_delay; - if (si->sta->aggressiveness >= 3) + if (si->aggressiveness >= 3) disassoc_timer = (si->kick_time - current_time) / usteer_local_node_get_beacon_interval(ln); else disassoc_timer = 0; diff --git a/local_node.c b/local_node.c index 1227115..1ec8407 100644 --- a/local_node.c +++ b/local_node.c @@ -184,7 +184,7 @@ usteer_handle_bss_tm_response(struct usteer_local_node *ln, struct blob_attr *ms si->bss_transition_response.status_code = blobmsg_get_u8(tb[BSS_TM_RESPONSE_STATUS_CODE]); si->bss_transition_response.timestamp = current_time; - if (si->bss_transition_response.status_code && si->kick_time && si->sta->aggressiveness) { + if (si->bss_transition_response.status_code && si->kick_time && si->aggressiveness) { /* Cancel imminent kick in case BSS transition was rejected */ si->kick_time = 0; MSG(VERBOSE, "Kick canceled because transition rejected by station " MAC_ADDR_FMT "\n", MAC_ADDR_DATA(si->sta->addr)); diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer index da9ab65..42c3214 100755 --- a/openwrt/usteer/files/etc/init.d/usteer +++ b/openwrt/usteer/files/etc/init.d/usteer @@ -72,7 +72,6 @@ uci_usteer() { uci_option_to_json_bool "$cfg" known_stations 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 uci_option_to_json_string_array "$cfg" event_log_types for opt in \ @@ -97,6 +96,35 @@ uci_usteer() { } +uci_usteer_ssid() { + local cfg="$1" + local ssid + + config_get ssid "$cfg" ssid + [ -n "$ssid" ] || return + + json_add_object "" + json_add_string ssid "$ssid" + + uci_option_to_json_bool "$cfg" load_kick_enabled + uci_option_to_json_string_array "$cfg" aggressiveness_mac_list + + for opt in \ + min_snr min_snr_kick_delay min_connect_snr signal_diff_threshold \ + roam_scan_snr roam_scan_tries roam_scan_timeout roam_scan_interval \ + roam_trigger_snr roam_trigger_interval \ + aggressiveness \ + band_steering_threshold band_steering_interval \ + band_steering_min_snr band_steering_signal_threshold \ + load_kick_threshold load_kick_delay load_kick_min_clients \ + load_kick_reason_code + do + uci_option_to_json "$cfg" "$opt" + done + + json_close_object +} + load_config() { [ "$ENABLED" -gt 0 ] || return @@ -112,6 +140,10 @@ load_config() { config_load usteer config_foreach uci_usteer usteer + json_add_array ssid_configs + config_foreach uci_usteer_ssid usteer_ssid + json_close_array + ubus call usteer set_config "$(json_dump)" } diff --git a/policy.c b/policy.c index b21166c..cdf2e50 100644 --- a/policy.c +++ b/policy.c @@ -21,6 +21,7 @@ #include "node.h" #include "event.h" #include "known.h" +#include "ssid_config.h" static bool below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new) @@ -34,9 +35,9 @@ below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new return false; if (ref_5g && !node_5g) - n_assoc_new += config.band_steering_threshold; + n_assoc_new += SSID_CFG(node_cur->ssid, band_steering_threshold); else if (!ref_5g && node_5g) - n_assoc_cur += config.band_steering_threshold; + n_assoc_cur += SSID_CFG(node_cur->ssid, band_steering_threshold); n_assoc_new += config.load_balancing_threshold; @@ -44,12 +45,12 @@ below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new } static bool -better_signal_strength(int signal_cur, int signal_new) +better_signal_strength(const char *ssid, int signal_cur, int signal_new) { - const bool is_better = signal_new - signal_cur - > (int) config.signal_diff_threshold; + uint32_t threshold = SSID_CFG(ssid, signal_diff_threshold); + const bool is_better = signal_new - signal_cur > (int) threshold; - if (!config.signal_diff_threshold) + if (!threshold) return false; return is_better; @@ -58,8 +59,8 @@ better_signal_strength(int signal_cur, int signal_new) static bool below_load_threshold(struct usteer_node *node) { - return node->n_assoc >= config.load_kick_min_clients && - node->load > config.load_kick_threshold; + return node->n_assoc >= SSID_CFG(node->ssid, load_kick_min_clients) && + node->load > SSID_CFG(node->ssid, load_kick_threshold); } static bool @@ -77,12 +78,15 @@ usteer_policy_node_below_max_assoc(struct usteer_node *node) static bool over_min_signal(struct usteer_node *node, int signal) { - if (config.min_snr && signal < usteer_snr_to_signal(node, config.min_snr)) + int32_t min_snr = SSID_CFG(node->ssid, min_snr); + int32_t roam_trigger_snr = SSID_CFG(node->ssid, roam_trigger_snr); + + if (min_snr && signal < usteer_snr_to_signal(node, min_snr)) return false; - if (config.roam_trigger_snr && signal < usteer_snr_to_signal(node, config.roam_trigger_snr)) + if (roam_trigger_snr && signal < usteer_snr_to_signal(node, roam_trigger_snr)) return false; - + return true; } @@ -105,7 +109,7 @@ is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new) !below_assoc_threshold(new_node, current_node)) reasons |= (1 << UEV_SELECT_REASON_NUM_ASSOC); - if (better_signal_strength(current_signal, new_signal)) + if (better_signal_strength(current_node->ssid, current_signal, new_signal)) reasons |= (1 << UEV_SELECT_REASON_SIGNAL); if (has_better_load(current_node, new_node) && @@ -116,7 +120,7 @@ is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new) } /* - * Optional fallback (known_stations): stations that never probe or + * Optional fallback (known_stations, per-SSID or global): stations that never probe or * report RRM measurements once associated never generate live sta_info * data on any node but their current one, so the scan in * find_better_candidate() below can never find them a candidate no @@ -270,6 +274,7 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) struct uevent ev = { .si_cur = si, }; + int32_t min_snr = SSID_CFG(si->node->ssid, min_snr); int min_signal; bool ret = true; bool exploratory; @@ -286,10 +291,10 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) * * Otherwise, the client potentially ends up in a assoc - kick loop. */ - if (config.min_snr && si->signal < usteer_snr_to_signal(si->node, config.min_snr)) { + if (min_snr && si->signal < usteer_snr_to_signal(si->node, min_snr)) { ev.reason = UEV_REASON_LOW_SIGNAL; ev.threshold.cur = si->signal; - ev.threshold.ref = usteer_snr_to_signal(si->node, config.min_snr); + ev.threshold.ref = usteer_snr_to_signal(si->node, min_snr); ret = false; goto out; } else if (!config.assoc_steering) { @@ -297,7 +302,7 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) } } - min_signal = usteer_snr_to_signal(si->node, config.min_connect_snr); + min_signal = usteer_snr_to_signal(si->node, SSID_CFG(si->node->ssid, min_connect_snr)); if (si->signal < min_signal) { ev.reason = UEV_REASON_LOW_SIGNAL; ev.threshold.cur = si->signal; @@ -390,9 +395,11 @@ usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state, static void usteer_roam_sm_start_scan(struct sta_info *si, struct uevent *ev) { + uint32_t roam_scan_timeout = SSID_CFG(si->node->ssid, roam_scan_timeout); + /* Start scanning in case we are not timeout-constrained or timeout has expired */ - if (!config.roam_scan_timeout || - current_time > si->roam_scan_timeout_start + config.roam_scan_timeout) { + if (!roam_scan_timeout || + current_time > si->roam_scan_timeout_start + roam_scan_timeout) { usteer_roam_set_state(si, ROAM_TRIGGER_SCAN, ev); return; } @@ -411,7 +418,7 @@ static struct sta_info * usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum roam_trigger_state next_state, bool *exploratory) { - uint64_t max_age = 2 * config.roam_scan_interval; + uint64_t max_age = 2 * SSID_CFG(si->node->ssid, roam_scan_interval); struct sta_info *candidate; if (max_age > current_time - si->roam_scan_start) @@ -431,6 +438,7 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) uint32_t disassoc_timer; uint32_t validity_period; bool exploratory = false; + uint32_t roam_scan_tries = SSID_CFG(si->node->ssid, roam_scan_tries); struct uevent ev = { .si_cur = si, }; @@ -446,12 +454,12 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) break; /* Only scan every scan-interval */ - if (current_time - si->roam_event < config.roam_scan_interval) + if (current_time - si->roam_event < SSID_CFG(si->node->ssid, roam_scan_interval)) break; /* Check if no node was found within roam_scan_tries tries */ - if (config.roam_scan_tries && si->roam_tries >= config.roam_scan_tries) { - if (!config.roam_scan_timeout) { + if (roam_scan_tries && si->roam_tries >= roam_scan_tries) { + if (!SSID_CFG(si->node->ssid, roam_scan_timeout)) { usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE, &ev); } else { /* Set timeout until roam_scans are paused */ @@ -498,10 +506,10 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) * be force-kicked - that would disconnect a station that may * still be perfectly happy where it is, on the strength of a * placeholder reading that was never actually observed. */ - if (si->sta->aggressiveness >= 2 && !exploratory) { + if (si->aggressiveness >= 2 && !exploratory) { if (!si->kick_time) si->kick_time = current_time + config.roam_kick_delay; - if (si->sta->aggressiveness >= 3) + if (si->aggressiveness >= 3) disassoc_timer = (si->kick_time - current_time) / usteer_local_node_get_beacon_interval(ln); else disassoc_timer = 0; @@ -528,7 +536,7 @@ bool usteer_policy_can_perform_roam(struct sta_info *si) return false; /* Only trigger for STA with active roaming */ - if (!si->sta->aggressiveness) + if (!si->aggressiveness) return false; /* Skip on pending kick */ @@ -548,11 +556,11 @@ bool usteer_policy_can_perform_roam(struct sta_info *si) return false; /* Skip on previous kick attempt */ - if (current_time - si->roam_kick < config.roam_trigger_interval) + if (current_time - si->roam_kick < SSID_CFG(si->node->ssid, roam_trigger_interval)) return false; /* Skip if connection is established shorter than the trigger-interval */ - if (current_time - si->connected_since < config.roam_trigger_interval) + if (current_time - si->connected_since < SSID_CFG(si->node->ssid, roam_trigger_interval)) return false; return true; @@ -575,12 +583,14 @@ static void usteer_local_node_roam_check(struct usteer_local_node *ln, struct uevent *ev) { struct sta_info *si; + int32_t roam_scan_snr = SSID_CFG(ln->node.ssid, roam_scan_snr); + int32_t roam_trigger_snr = SSID_CFG(ln->node.ssid, roam_trigger_snr); int min_signal; - if (config.roam_scan_snr) - min_signal = config.roam_scan_snr; - else if (config.roam_trigger_snr) - min_signal = config.roam_trigger_snr; + if (roam_scan_snr) + min_signal = roam_scan_snr; + else if (roam_trigger_snr) + min_signal = roam_trigger_snr; else return; @@ -605,17 +615,18 @@ usteer_local_node_roam_check(struct usteer_local_node *ln, struct uevent *ev) static void usteer_local_node_snr_kick(struct usteer_local_node *ln) { - unsigned int min_count = DIV_ROUND_UP(config.min_snr_kick_delay, config.local_sta_update); + int32_t min_snr = SSID_CFG(ln->node.ssid, min_snr); + unsigned int min_count = DIV_ROUND_UP(SSID_CFG(ln->node.ssid, min_snr_kick_delay), config.local_sta_update); struct uevent ev = { .node_local = &ln->node, }; struct sta_info *si; int min_signal; - if (!config.min_snr) + if (!min_snr) return; - min_signal = usteer_snr_to_signal(&ln->node, config.min_snr); + min_signal = usteer_snr_to_signal(&ln->node, min_snr); ev.threshold.ref = min_signal; list_for_each_entry(si, &ln->node.sta_info, node_list) { @@ -652,20 +663,23 @@ usteer_local_node_load_kick(struct usteer_local_node *ln) struct uevent ev = { .node_local = &ln->node, }; - unsigned int min_count = DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update); + bool load_kick_enabled = SSID_CFG(node->ssid, load_kick_enabled); + uint32_t load_kick_threshold = SSID_CFG(node->ssid, load_kick_threshold); + uint32_t load_kick_delay = SSID_CFG(node->ssid, load_kick_delay); + uint32_t load_kick_min_clients = SSID_CFG(node->ssid, load_kick_min_clients); + unsigned int min_count = DIV_ROUND_UP(load_kick_delay, config.local_sta_update); - if (!config.load_kick_enabled || !config.load_kick_threshold || - !config.load_kick_delay) + if (!load_kick_enabled || !load_kick_threshold || !load_kick_delay) return; - if (node->load < config.load_kick_threshold) { + if (node->load < load_kick_threshold) { if (!ln->load_thr_count) return; ln->load_thr_count = 0; ev.type = UEV_LOAD_KICK_RESET; ev.threshold.cur = node->load; - ev.threshold.ref = config.load_kick_threshold; + ev.threshold.ref = load_kick_threshold; goto out; } @@ -675,15 +689,15 @@ usteer_local_node_load_kick(struct usteer_local_node *ln) ev.type = UEV_LOAD_KICK_TRIGGER; ev.threshold.cur = node->load; - ev.threshold.ref = config.load_kick_threshold; + ev.threshold.ref = load_kick_threshold; goto out; } ln->load_thr_count = 0; - if (node->n_assoc < config.load_kick_min_clients) { + if (node->n_assoc < load_kick_min_clients) { ev.type = UEV_LOAD_KICK_MIN_CLIENTS; ev.threshold.cur = node->n_assoc; - ev.threshold.ref = config.load_kick_min_clients; + ev.threshold.ref = load_kick_min_clients; goto out; } @@ -721,7 +735,7 @@ usteer_local_node_load_kick(struct usteer_local_node *ln) ev.si_other = candidate; ev.count = kick1->kick_count; - usteer_ubus_kick_client(kick1, config.load_kick_reason_code); + usteer_ubus_kick_client(kick1, SSID_CFG(node->ssid, load_kick_reason_code)); out: usteer_event(&ev); diff --git a/ssid_config.c b/ssid_config.c new file mode 100644 index 0000000..caab7d3 --- /dev/null +++ b/ssid_config.c @@ -0,0 +1,197 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#include + +#include "ssid_config.h" + +AVL_TREE(ssid_configs, avl_strcmp, false, NULL); + +struct usteer_ssid_config * +usteer_ssid_config_get(const char *ssid) +{ + struct usteer_ssid_config *sc; + + if (!ssid || !*ssid) + return NULL; + + return avl_find_element(&ssid_configs, ssid, sc, avl); +} + +static void +usteer_ssid_config_free(struct usteer_ssid_config *sc) +{ + avl_delete(&ssid_configs, &sc->avl); + free(sc->aggressiveness_mac_list); + /* sc->avl.key (ssid_buf) is NOT a separate allocation - it was + * carved out of the same calloc_a() block as sc itself, so freeing + * sc below also releases it. Freeing it separately here corrupts + * the heap (this was crashing every second set_config call, right + * after the first reconfiguration's cleanup pass ran). */ + free(sc); +} + +enum { + SCFG_SSID, + SCFG_MIN_SNR, + SCFG_MIN_SNR_KICK_DELAY, + SCFG_MIN_CONNECT_SNR, + SCFG_SIGNAL_DIFF_THRESHOLD, + SCFG_ROAM_SCAN_SNR, + SCFG_ROAM_SCAN_TRIES, + SCFG_ROAM_SCAN_TIMEOUT, + SCFG_ROAM_SCAN_INTERVAL, + SCFG_ROAM_TRIGGER_SNR, + SCFG_ROAM_TRIGGER_INTERVAL, + SCFG_AGGRESSIVENESS, + SCFG_AGGRESSIVENESS_MAC_LIST, + SCFG_BAND_STEERING_THRESHOLD, + SCFG_BAND_STEERING_INTERVAL, + SCFG_BAND_STEERING_MIN_SNR, + SCFG_BAND_STEERING_SIGNAL_THRESHOLD, + SCFG_LOAD_KICK_ENABLED, + SCFG_LOAD_KICK_THRESHOLD, + SCFG_LOAD_KICK_DELAY, + SCFG_LOAD_KICK_MIN_CLIENTS, + SCFG_LOAD_KICK_REASON_CODE, + __SCFG_MAX +}; + +static const struct blobmsg_policy ssid_cfg_policy[__SCFG_MAX] = { + [SCFG_SSID] = { "ssid", BLOBMSG_TYPE_STRING }, + [SCFG_MIN_SNR] = { "min_snr", BLOBMSG_TYPE_INT32 }, + [SCFG_MIN_SNR_KICK_DELAY] = { "min_snr_kick_delay", BLOBMSG_TYPE_INT32 }, + [SCFG_MIN_CONNECT_SNR] = { "min_connect_snr", BLOBMSG_TYPE_INT32 }, + [SCFG_SIGNAL_DIFF_THRESHOLD] = { "signal_diff_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_SCAN_SNR] = { "roam_scan_snr", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_SCAN_TRIES] = { "roam_scan_tries", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_SCAN_TIMEOUT] = { "roam_scan_timeout", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_SCAN_INTERVAL] = { "roam_scan_interval", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_TRIGGER_SNR] = { "roam_trigger_snr", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_TRIGGER_INTERVAL] = { "roam_trigger_interval", BLOBMSG_TYPE_INT32 }, + [SCFG_AGGRESSIVENESS] = { "aggressiveness", BLOBMSG_TYPE_INT32 }, + [SCFG_AGGRESSIVENESS_MAC_LIST] = { "aggressiveness_mac_list", BLOBMSG_TYPE_ARRAY }, + [SCFG_BAND_STEERING_THRESHOLD] = { "band_steering_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_BAND_STEERING_INTERVAL] = { "band_steering_interval", BLOBMSG_TYPE_INT32 }, + [SCFG_BAND_STEERING_MIN_SNR] = { "band_steering_min_snr", BLOBMSG_TYPE_INT32 }, + [SCFG_BAND_STEERING_SIGNAL_THRESHOLD] = { "band_steering_signal_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_LOAD_KICK_ENABLED] = { "load_kick_enabled", BLOBMSG_TYPE_BOOL }, + [SCFG_LOAD_KICK_THRESHOLD] = { "load_kick_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_LOAD_KICK_DELAY] = { "load_kick_delay", BLOBMSG_TYPE_INT32 }, + [SCFG_LOAD_KICK_MIN_CLIENTS] = { "load_kick_min_clients", BLOBMSG_TYPE_INT32 }, + [SCFG_LOAD_KICK_REASON_CODE] = { "load_kick_reason_code", BLOBMSG_TYPE_INT32 }, +}; + +#define SCFG_INT(_sc, _tb, _field, _idx) \ + (_sc)->_field = (_tb)[_idx] ? blobmsg_get_u32(_tb[_idx]) : config._field +#define SCFG_SINT(_sc, _tb, _field, _idx) \ + (_sc)->_field = (_tb)[_idx] ? (int32_t) blobmsg_get_u32(_tb[_idx]) : config._field +#define SCFG_BOOL(_sc, _tb, _field, _idx) \ + (_sc)->_field = (_tb)[_idx] ? blobmsg_get_bool(_tb[_idx]) : config._field +/* Blob-typed fields can't be shared with the global config by pointer + * (config._field can be freed/replaced by a later config_set_*() call) - + * always take an owned copy, either of the override or of the current + * global value, so it stays valid for the lifetime of this sc entry. */ +#define SCFG_LIST(_sc, _tb, _field, _idx) \ + do { \ + struct blob_attr *__src = (_tb)[_idx] ? (_tb)[_idx] : config._field; \ + (_sc)->_field = __src ? blob_memdup(__src) : NULL; \ + } while (0) + +void config_set_ssid_configs(struct blob_attr *data) +{ + struct usteer_ssid_config *sc, *tmp; + struct blob_attr *cur; + int rem; + + avl_for_each_element_safe(&ssid_configs, sc, avl, tmp) + usteer_ssid_config_free(sc); + + if (!data) + return; + + blobmsg_for_each_attr(cur, data, rem) { + struct blob_attr *tb[__SCFG_MAX]; + char *ssid_buf; + + blobmsg_parse(ssid_cfg_policy, __SCFG_MAX, tb, blobmsg_data(cur), blobmsg_data_len(cur)); + if (!tb[SCFG_SSID]) + continue; + + sc = calloc_a(sizeof(*sc), &ssid_buf, strlen(blobmsg_get_string(tb[SCFG_SSID])) + 1); + strcpy(ssid_buf, blobmsg_get_string(tb[SCFG_SSID])); + sc->avl.key = ssid_buf; + + SCFG_SINT(sc, tb, min_snr, SCFG_MIN_SNR); + SCFG_INT(sc, tb, min_snr_kick_delay, SCFG_MIN_SNR_KICK_DELAY); + SCFG_SINT(sc, tb, min_connect_snr, SCFG_MIN_CONNECT_SNR); + SCFG_INT(sc, tb, signal_diff_threshold, SCFG_SIGNAL_DIFF_THRESHOLD); + SCFG_SINT(sc, tb, roam_scan_snr, SCFG_ROAM_SCAN_SNR); + SCFG_INT(sc, tb, roam_scan_tries, SCFG_ROAM_SCAN_TRIES); + SCFG_INT(sc, tb, roam_scan_timeout, SCFG_ROAM_SCAN_TIMEOUT); + SCFG_INT(sc, tb, roam_scan_interval, SCFG_ROAM_SCAN_INTERVAL); + SCFG_SINT(sc, tb, roam_trigger_snr, SCFG_ROAM_TRIGGER_SNR); + SCFG_INT(sc, tb, roam_trigger_interval, SCFG_ROAM_TRIGGER_INTERVAL); + SCFG_INT(sc, tb, aggressiveness, SCFG_AGGRESSIVENESS); + SCFG_LIST(sc, tb, aggressiveness_mac_list, SCFG_AGGRESSIVENESS_MAC_LIST); + SCFG_INT(sc, tb, band_steering_threshold, SCFG_BAND_STEERING_THRESHOLD); + SCFG_INT(sc, tb, band_steering_interval, SCFG_BAND_STEERING_INTERVAL); + SCFG_SINT(sc, tb, band_steering_min_snr, SCFG_BAND_STEERING_MIN_SNR); + SCFG_INT(sc, tb, band_steering_signal_threshold, SCFG_BAND_STEERING_SIGNAL_THRESHOLD); + SCFG_BOOL(sc, tb, load_kick_enabled, SCFG_LOAD_KICK_ENABLED); + SCFG_INT(sc, tb, load_kick_threshold, SCFG_LOAD_KICK_THRESHOLD); + SCFG_INT(sc, tb, load_kick_delay, SCFG_LOAD_KICK_DELAY); + SCFG_INT(sc, tb, load_kick_min_clients, SCFG_LOAD_KICK_MIN_CLIENTS); + SCFG_INT(sc, tb, load_kick_reason_code, SCFG_LOAD_KICK_REASON_CODE); + + avl_insert(&ssid_configs, &sc->avl); + } +} + +void config_get_ssid_configs(struct blob_buf *buf) +{ + struct usteer_ssid_config *sc; + void *c, *e; + + c = blobmsg_open_array(buf, "ssid_configs"); + avl_for_each_element(&ssid_configs, sc, avl) { + e = blobmsg_open_table(buf, NULL); + blobmsg_add_string(buf, "ssid", sc->avl.key); + blobmsg_add_u32(buf, "min_snr", sc->min_snr); + blobmsg_add_u32(buf, "min_snr_kick_delay", sc->min_snr_kick_delay); + blobmsg_add_u32(buf, "min_connect_snr", sc->min_connect_snr); + blobmsg_add_u32(buf, "signal_diff_threshold", sc->signal_diff_threshold); + blobmsg_add_u32(buf, "roam_scan_snr", sc->roam_scan_snr); + blobmsg_add_u32(buf, "roam_scan_tries", sc->roam_scan_tries); + blobmsg_add_u32(buf, "roam_scan_timeout", sc->roam_scan_timeout); + blobmsg_add_u32(buf, "roam_scan_interval", sc->roam_scan_interval); + blobmsg_add_u32(buf, "roam_trigger_snr", sc->roam_trigger_snr); + blobmsg_add_u32(buf, "roam_trigger_interval", sc->roam_trigger_interval); + blobmsg_add_u32(buf, "aggressiveness", sc->aggressiveness); + if (sc->aggressiveness_mac_list) + blobmsg_add_blob(buf, sc->aggressiveness_mac_list); + blobmsg_add_u32(buf, "band_steering_threshold", sc->band_steering_threshold); + blobmsg_add_u32(buf, "band_steering_interval", sc->band_steering_interval); + blobmsg_add_u32(buf, "band_steering_min_snr", sc->band_steering_min_snr); + blobmsg_add_u32(buf, "band_steering_signal_threshold", sc->band_steering_signal_threshold); + blobmsg_add_u8(buf, "load_kick_enabled", sc->load_kick_enabled); + blobmsg_add_u32(buf, "load_kick_threshold", sc->load_kick_threshold); + blobmsg_add_u32(buf, "load_kick_delay", sc->load_kick_delay); + blobmsg_add_u32(buf, "load_kick_min_clients", sc->load_kick_min_clients); + blobmsg_add_u32(buf, "load_kick_reason_code", sc->load_kick_reason_code); + blobmsg_close_table(buf, e); + } + blobmsg_close_array(buf, c); +} diff --git a/ssid_config.h b/ssid_config.h new file mode 100644 index 0000000..b53c63f --- /dev/null +++ b/ssid_config.h @@ -0,0 +1,84 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __USTEER_SSID_CONFIG_H +#define __USTEER_SSID_CONFIG_H + +#include "usteer.h" + +/* + * Optional per-SSID overrides of the roaming-policy subset of + * usteer_config. Every field here mirrors a same-named field in + * usteer_config exactly (same type, same meaning) - the only + * difference is that these apply to stations on one specific SSID + * instead of network-wide. + * + * A SSID with no entry here simply uses the global usteer_config value + * for every field, unchanged - existing single-SSID or "same policy + * everywhere" setups need no configuration change at all. Use + * SSID_CFG(ssid, field) everywhere a call site used to read + * config.field directly, to transparently get the per-SSID override + * when one exists. + */ +struct usteer_ssid_config { + struct avl_node avl; /* key: heap-allocated copy of the SSID string */ + + int32_t min_snr; + uint32_t min_snr_kick_delay; + int32_t min_connect_snr; + uint32_t signal_diff_threshold; + + int32_t roam_scan_snr; + uint32_t roam_scan_tries; + uint32_t roam_scan_timeout; + uint32_t roam_scan_interval; + + int32_t roam_trigger_snr; + uint32_t roam_trigger_interval; + + uint32_t aggressiveness; + struct blob_attr *aggressiveness_mac_list; + + uint32_t band_steering_threshold; + uint32_t band_steering_interval; + int32_t band_steering_min_snr; + uint32_t band_steering_signal_threshold; + + bool load_kick_enabled; + uint32_t load_kick_threshold; + uint32_t load_kick_delay; + uint32_t load_kick_min_clients; + uint32_t load_kick_reason_code; +}; + +extern struct avl_tree ssid_configs; + +/* NULL if ssid has no override - callers fall back to global config. */ +struct usteer_ssid_config *usteer_ssid_config_get(const char *ssid); + +/* + * ssid may be NULL or empty (e.g. a node whose SSID hasn't been + * populated yet) - always falls back to the global value in that case. + */ +#define SSID_CFG(ssid, field) \ + ({ \ + struct usteer_ssid_config *__sc = (ssid) ? usteer_ssid_config_get(ssid) : NULL; \ + __sc ? __sc->field : config.field; \ + }) + +void config_set_ssid_configs(struct blob_attr *data); +void config_get_ssid_configs(struct blob_buf *buf); + +#endif diff --git a/sta.c b/sta.c index 059e987..abb55ff 100644 --- a/sta.c +++ b/sta.c @@ -18,6 +18,8 @@ */ #include "usteer.h" +#include "known.h" +#include "ssid_config.h" static int avl_macaddr_cmp(const void *k1, const void *k2, void *ptr) @@ -77,17 +79,18 @@ usteer_sta_info_timeout(struct usteer_timeout_queue *q, struct usteer_timeout *t } static void -usteer_sta_update_aggressiveness(struct sta *sta) +usteer_sta_update_aggressiveness(struct sta_info *si) { struct blob_attr *cur; int rem; char sta_mac[18]; char config_entry[20]; char config_mac[18]; - - sprintf(sta_mac, MAC_ADDR_FMT, MAC_ADDR_DATA(sta->addr)); - sta->aggressiveness = config.aggressiveness; - blobmsg_for_each_attr(cur, config.aggressiveness_mac_list, rem) { + const char *ssid = si->node->ssid; + + sprintf(sta_mac, MAC_ADDR_FMT, MAC_ADDR_DATA(si->sta->addr)); + si->aggressiveness = SSID_CFG(ssid, aggressiveness); + blobmsg_for_each_attr(cur, SSID_CFG(ssid, aggressiveness_mac_list), rem) { strcpy(config_entry, blobmsg_get_string(cur)); if (strlen(config_entry) != 19) continue; @@ -95,7 +98,7 @@ usteer_sta_update_aggressiveness(struct sta *sta) config_mac[17] = '\0'; if (strcmp(config_mac, sta_mac) != 0) continue; - sta->aggressiveness = config_entry[18] - '0'; + si->aggressiveness = config_entry[18] - '0'; break; } } @@ -129,7 +132,7 @@ usteer_sta_info_get(struct sta *sta, struct usteer_node *node, bool *create) si->created = current_time; *create = true; - usteer_sta_update_aggressiveness(sta); + usteer_sta_update_aggressiveness(si); /* Node is by default not connected. */ usteer_sta_disconnected(si); @@ -190,6 +193,21 @@ usteer_sta_info_update(struct sta_info *si, int signal, bool avg) if (signal != NO_SIGNAL) { si->signal = signal; usteer_band_steering_sta_update(si); + + /* Only record known-station data from the periodic nl80211 + * poll of stations hostapd currently reports as associated + * (avg == true; see nl80211_update_sta()) - not from bare + * PROBE/AUTH/ASSOC events (avg == false), which are + * frequently sent from randomized, throwaway MAC addresses + * (privacy MAC randomization on phones/tablets scanning + * nearby networks without ever connecting) and would + * otherwise flood the known-station list with junk that is + * never seen again. Note si->connected is not yet updated + * to STA_CONNECTED at this point in the nl80211 poll cycle + * (see usteer_local_node_set_assoc()), so it can't be used + * as the gate here. */ + if (si->node->type == NODE_TYPE_LOCAL && avg) + usteer_known_update(si->node, si->sta->addr, signal); } si->seen = current_time; diff --git a/ubus.c b/ubus.c index d57922f..26b56a2 100644 --- a/ubus.c +++ b/ubus.c @@ -28,6 +28,7 @@ #include "node.h" #include "event.h" #include "known.h" +#include "ssid_config.h" static struct blob_buf b; static KVLIST(host_info, kvlist_blob_len); @@ -224,7 +225,8 @@ struct cfg_item { _cfg(ARRAY_CB, event_log_types), \ _cfg(ARRAY_CB, ssid_list), \ _cfg(BOOL, known_stations), \ - _cfg(U32, known_stations_timeout) + _cfg(U32, known_stations_timeout), \ + _cfg(ARRAY_CB, ssid_configs) enum cfg_items { #define _cfg(_type, _name) CFG_##_name diff --git a/usteer.h b/usteer.h index 4cb0bef..6e84afc 100644 --- a/usteer.h +++ b/usteer.h @@ -255,6 +255,12 @@ struct sta_info { struct usteer_node *node; struct sta *sta; + /* Computed from this node's SSID (see usteer_sta_update_aggressiveness() + * in sta.c) - lives here rather than on struct sta because a station + * seen on multiple SSIDs must use each SSID's own aggressiveness for + * decisions made on that SSID's nodes. */ + uint32_t aggressiveness; + struct usteer_timeout timeout; struct sta_info_stats stats[__EVENT_TYPE_MAX]; @@ -307,8 +313,6 @@ struct sta { uint8_t seen_2ghz : 1; uint8_t seen_5ghz : 1; - uint32_t aggressiveness; - uint8_t addr[6]; }; From f66af8b4081b24d6190da7eadc1121ab111f9d50 Mon Sep 17 00:00:00 2001 From: Grzegorz Kaczor Date: Sun, 26 Jul 2026 08:56:10 +0200 Subject: [PATCH 3/5] Add band steering enable/disable toggle Band steering previously had no explicit on/off switch - it was only implicitly active whenever band_steering_interval was non-zero. Add a real band_steering_enabled bool (global and per-SSID, following the same pattern as load_kick_enabled) that gates the actual steering action in usteer_band_steering_perform_steer(), alongside the existing interval check. Defaults to true, preserving existing behavior for configs that already rely on a non-zero band_steering_interval. Signed-off-by: Grzegorz Kaczor --- band_steering.c | 3 ++- main.c | 1 + openwrt/usteer/files/etc/init.d/usteer | 2 ++ ssid_config.c | 4 ++++ ssid_config.h | 1 + ubus.c | 1 + usteer.h | 1 + 7 files changed, 12 insertions(+), 1 deletion(-) diff --git a/band_steering.c b/band_steering.c index 8eeaf5d..4e09b60 100644 --- a/band_steering.c +++ b/band_steering.c @@ -84,13 +84,14 @@ static bool usteer_band_steering_has_target_iface(struct usteer_local_node *ln) void usteer_band_steering_perform_steer(struct usteer_local_node *ln) { + bool band_steering_enabled = SSID_CFG(ln->node.ssid, band_steering_enabled); uint32_t band_steering_interval = SSID_CFG(ln->node.ssid, band_steering_interval); unsigned int min_count = DIV_ROUND_UP(band_steering_interval, config.local_sta_update); struct sta_info *si; uint32_t disassoc_timer; uint32_t validity_period; - if (!band_steering_interval) + if (!band_steering_enabled || !band_steering_interval) return; /* Band-Steering is only available on 2.4 GHz interfaces */ diff --git a/main.c b/main.c index af96f66..48522b3 100644 --- a/main.c +++ b/main.c @@ -102,6 +102,7 @@ void usteer_init_defaults(void) config.steer_reject_timeout = 60000; + config.band_steering_enabled = true; config.band_steering_interval = 30000; config.band_steering_min_snr = -60; config.band_steering_signal_threshold = 5; diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer index 42c3214..adecf46 100755 --- a/openwrt/usteer/files/etc/init.d/usteer +++ b/openwrt/usteer/files/etc/init.d/usteer @@ -70,6 +70,7 @@ uci_usteer() { uci_option_to_json_bool "$cfg" load_kick_enabled uci_option_to_json_bool "$cfg" assoc_steering uci_option_to_json_bool "$cfg" known_stations + uci_option_to_json_bool "$cfg" band_steering_enabled 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" event_log_types @@ -107,6 +108,7 @@ uci_usteer_ssid() { json_add_string ssid "$ssid" uci_option_to_json_bool "$cfg" load_kick_enabled + uci_option_to_json_bool "$cfg" band_steering_enabled uci_option_to_json_string_array "$cfg" aggressiveness_mac_list for opt in \ diff --git a/ssid_config.c b/ssid_config.c index caab7d3..8b1ed86 100644 --- a/ssid_config.c +++ b/ssid_config.c @@ -58,6 +58,7 @@ enum { SCFG_AGGRESSIVENESS, SCFG_AGGRESSIVENESS_MAC_LIST, SCFG_BAND_STEERING_THRESHOLD, + SCFG_BAND_STEERING_ENABLED, SCFG_BAND_STEERING_INTERVAL, SCFG_BAND_STEERING_MIN_SNR, SCFG_BAND_STEERING_SIGNAL_THRESHOLD, @@ -84,6 +85,7 @@ static const struct blobmsg_policy ssid_cfg_policy[__SCFG_MAX] = { [SCFG_AGGRESSIVENESS] = { "aggressiveness", BLOBMSG_TYPE_INT32 }, [SCFG_AGGRESSIVENESS_MAC_LIST] = { "aggressiveness_mac_list", BLOBMSG_TYPE_ARRAY }, [SCFG_BAND_STEERING_THRESHOLD] = { "band_steering_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_BAND_STEERING_ENABLED] = { "band_steering_enabled", BLOBMSG_TYPE_BOOL }, [SCFG_BAND_STEERING_INTERVAL] = { "band_steering_interval", BLOBMSG_TYPE_INT32 }, [SCFG_BAND_STEERING_MIN_SNR] = { "band_steering_min_snr", BLOBMSG_TYPE_INT32 }, [SCFG_BAND_STEERING_SIGNAL_THRESHOLD] = { "band_steering_signal_threshold", BLOBMSG_TYPE_INT32 }, @@ -147,6 +149,7 @@ void config_set_ssid_configs(struct blob_attr *data) SCFG_INT(sc, tb, aggressiveness, SCFG_AGGRESSIVENESS); SCFG_LIST(sc, tb, aggressiveness_mac_list, SCFG_AGGRESSIVENESS_MAC_LIST); SCFG_INT(sc, tb, band_steering_threshold, SCFG_BAND_STEERING_THRESHOLD); + SCFG_BOOL(sc, tb, band_steering_enabled, SCFG_BAND_STEERING_ENABLED); SCFG_INT(sc, tb, band_steering_interval, SCFG_BAND_STEERING_INTERVAL); SCFG_SINT(sc, tb, band_steering_min_snr, SCFG_BAND_STEERING_MIN_SNR); SCFG_INT(sc, tb, band_steering_signal_threshold, SCFG_BAND_STEERING_SIGNAL_THRESHOLD); @@ -183,6 +186,7 @@ void config_get_ssid_configs(struct blob_buf *buf) if (sc->aggressiveness_mac_list) blobmsg_add_blob(buf, sc->aggressiveness_mac_list); blobmsg_add_u32(buf, "band_steering_threshold", sc->band_steering_threshold); + blobmsg_add_u8(buf, "band_steering_enabled", sc->band_steering_enabled); blobmsg_add_u32(buf, "band_steering_interval", sc->band_steering_interval); blobmsg_add_u32(buf, "band_steering_min_snr", sc->band_steering_min_snr); blobmsg_add_u32(buf, "band_steering_signal_threshold", sc->band_steering_signal_threshold); diff --git a/ssid_config.h b/ssid_config.h index b53c63f..913371f 100644 --- a/ssid_config.h +++ b/ssid_config.h @@ -52,6 +52,7 @@ struct usteer_ssid_config { struct blob_attr *aggressiveness_mac_list; uint32_t band_steering_threshold; + bool band_steering_enabled; uint32_t band_steering_interval; int32_t band_steering_min_snr; uint32_t band_steering_signal_threshold; diff --git a/ubus.c b/ubus.c index 26b56a2..5275677 100644 --- a/ubus.c +++ b/ubus.c @@ -217,6 +217,7 @@ struct cfg_item { _cfg(U32, load_kick_delay), \ _cfg(U32, load_kick_min_clients), \ _cfg(U32, load_kick_reason_code), \ + _cfg(BOOL, band_steering_enabled), \ _cfg(U32, band_steering_interval), \ _cfg(I32, band_steering_min_snr), \ _cfg(U32, link_measurement_interval), \ diff --git a/usteer.h b/usteer.h index 6e84afc..bd6ce53 100644 --- a/usteer.h +++ b/usteer.h @@ -204,6 +204,7 @@ struct usteer_config { uint32_t roam_kick_delay; + bool band_steering_enabled; uint32_t band_steering_interval; int32_t band_steering_min_snr; uint32_t band_steering_signal_threshold; From 331fb2767ec7f81433e77063ed2a1f6f67deccdc Mon Sep 17 00:00:00 2001 From: Grzegorz Kaczor Date: Wed, 29 Jul 2026 08:52:12 +0200 Subject: [PATCH 4/5] Extend per-SSID configuration overrides to remaining policy fields Add sta_block_timeout, local_sta_timeout, max_retry_band, seen_policy_timeout, assoc_steering, probe_steering, max_neighbor_reports, load_balancing_threshold, steer_reject_timeout, roam_process_timeout, roam_kick_delay, initial_connect_delay and node_up_script as per-SSID overrides, mirroring the pattern already used for the SNR/band-steering/load-kick fields: each is consulted in a station+node scoped context (SSID_CFG(ssid, field)) that was previously only reading the network-wide global, and falls back to the global value when no override exists. node_up_script needed different handling than the others: its real storage isn't config.node_up_script (that struct field is dead) but a private static in local_node.c set via config_set_node_up_script(), so it can't use the SSID_CFG() macro's built-in "fall back to config.field" - the per-SSID override is resolved manually at the one call site instead, falling back to that static when unset. probe_steering was previously configurable only via a hardcoded default in usteer_init_defaults() (config.probe_steering = 0) - it was never wired into the global UCI config policy in ubus.c, so there was no way to actually turn it on. It's now settable per-SSID. Enum/policy/struct field order in ssid_config.h/.c mirrors struct usteer_config's declaration order throughout, to keep the two structures easy to diff against each other as fields are added. Signed-off-by: Grzegorz Kaczor --- band_steering.c | 2 +- local_node.c | 20 ++++-- openwrt/usteer/files/etc/init.d/usteer | 14 ++++- policy.c | 29 +++++---- remote.c | 5 +- ssid_config.c | 86 ++++++++++++++++++++++---- ssid_config.h | 37 ++++++++++- sta.c | 6 +- ubus.c | 5 +- 9 files changed, 160 insertions(+), 44 deletions(-) diff --git a/band_steering.c b/band_steering.c index 4e09b60..519f6b9 100644 --- a/band_steering.c +++ b/band_steering.c @@ -130,7 +130,7 @@ void usteer_band_steering_perform_steer(struct usteer_local_node *ln) validity_period = 10000 / usteer_local_node_get_beacon_interval(ln); /* ~ 10 seconds */ if (si->aggressiveness >= 2) { if (!si->kick_time) - si->kick_time = current_time + config.roam_kick_delay; + si->kick_time = current_time + SSID_CFG(si->node->ssid, roam_kick_delay); if (si->aggressiveness >= 3) disassoc_timer = (si->kick_time - current_time) / usteer_local_node_get_beacon_interval(ln); else diff --git a/local_node.c b/local_node.c index 1ec8407..9b70eb3 100644 --- a/local_node.c +++ b/local_node.c @@ -31,6 +31,7 @@ #include "usteer.h" #include "node.h" #include "known.h" +#include "ssid_config.h" AVL_TREE(local_nodes, avl_strcmp, false, NULL); static struct blob_buf b; @@ -377,7 +378,7 @@ usteer_local_node_assoc_update(struct sta_info *si, struct blob_attr *data) if (!remote_si) continue; - if (current_time - remote_si->last_connected < config.roam_process_timeout) { + if (current_time - remote_si->last_connected < SSID_CFG(si->node->ssid, roam_process_timeout)) { rn->node.roam_events.source++; /* Don't abort looking for roam sources here. * The client might have roamed via another node @@ -614,15 +615,17 @@ usteer_local_node_prepare_rrm_set(struct usteer_local_node *ln) int i = 0; void *c; + uint32_t max_neighbor_reports = SSID_CFG(ln->node.ssid, max_neighbor_reports); + c = blobmsg_open_array(&b, "list"); for_each_local_node(node) { - if (i >= config.max_neighbor_reports) + if (i >= max_neighbor_reports) break; if (usteer_add_rrm_data(ln, node)) i++; } - while (i < config.max_neighbor_reports) { + while (i < max_neighbor_reports) { node = usteer_node_get_next_neighbor(&ln->node, last_remote_neighbor); if (!node) { /* No more nodes available */ @@ -796,13 +799,18 @@ static void usteer_node_run_update_script(struct usteer_node *node) { struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node); + /* Not a plain SSID_CFG() lookup: the global fallback here is the + * node_up_script static above, not config.node_up_script (dead + * field) - see the comment on usteer_ssid_config.node_up_script. */ + struct usteer_ssid_config *sc = usteer_ssid_config_get(node->ssid); + const char *script = (sc && sc->node_up_script) ? sc->node_up_script : node_up_script; char *val; - if (!node_up_script) + if (!script) return; - val = alloca(strlen(node_up_script) + strlen(ln->iface) + 8); - sprintf(val, "%s '%s'", node_up_script, ln->iface); + val = alloca(strlen(script) + strlen(ln->iface) + 8); + sprintf(val, "%s '%s'", script, ln->iface); if (system(val)) MSG(INFO, "failed to execute %s\n", val); } diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer index adecf46..1504507 100755 --- a/openwrt/usteer/files/etc/init.d/usteer +++ b/openwrt/usteer/files/etc/init.d/usteer @@ -69,6 +69,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" probe_steering uci_option_to_json_bool "$cfg" known_stations uci_option_to_json_bool "$cfg" band_steering_enabled uci_option_to_json_string "$cfg" node_up_script @@ -109,15 +110,26 @@ uci_usteer_ssid() { uci_option_to_json_bool "$cfg" load_kick_enabled uci_option_to_json_bool "$cfg" band_steering_enabled + uci_option_to_json_bool "$cfg" assoc_steering + uci_option_to_json_bool "$cfg" probe_steering uci_option_to_json_string_array "$cfg" aggressiveness_mac_list + uci_option_to_json_string "$cfg" node_up_script for opt in \ + sta_block_timeout local_sta_timeout \ + max_retry_band seen_policy_timeout \ + max_neighbor_reports \ + load_balancing_threshold \ min_snr min_snr_kick_delay min_connect_snr signal_diff_threshold \ - roam_scan_snr roam_scan_tries roam_scan_timeout roam_scan_interval \ + steer_reject_timeout \ + roam_scan_snr roam_process_timeout \ + roam_scan_tries roam_scan_timeout roam_scan_interval \ roam_trigger_snr roam_trigger_interval \ + roam_kick_delay \ aggressiveness \ band_steering_threshold band_steering_interval \ band_steering_min_snr band_steering_signal_threshold \ + initial_connect_delay \ load_kick_threshold load_kick_delay load_kick_min_clients \ load_kick_reason_code do diff --git a/policy.c b/policy.c index cdf2e50..f83eb12 100644 --- a/policy.c +++ b/policy.c @@ -31,7 +31,9 @@ below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new bool ref_5g = node_cur->freq > 4000; bool node_5g = node_new->freq > 4000; - if (!config.load_balancing_threshold) + uint32_t load_balancing_threshold = SSID_CFG(node_cur->ssid, load_balancing_threshold); + + if (!load_balancing_threshold) return false; if (ref_5g && !node_5g) @@ -39,7 +41,7 @@ below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new else if (!ref_5g && node_5g) n_assoc_cur += SSID_CFG(node_cur->ssid, band_steering_threshold); - n_assoc_new += config.load_balancing_threshold; + n_assoc_new += load_balancing_threshold; return n_assoc_new <= n_assoc_cur; } @@ -220,7 +222,7 @@ find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t requi if (si == si_ref) continue; - if (current_time - si->seen > config.seen_policy_timeout) + if (current_time - si->seen > SSID_CFG(si_ref->node->ssid, seen_policy_timeout)) continue; if (strcmp(si->node->ssid, si_ref->node->ssid) != 0) @@ -279,7 +281,7 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) bool ret = true; bool exploratory; - if (type == EVENT_TYPE_PROBE && !config.probe_steering) + if (type == EVENT_TYPE_PROBE && !SSID_CFG(si->node->ssid, probe_steering)) goto out; if (type == EVENT_TYPE_AUTH) @@ -297,7 +299,7 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) ev.threshold.ref = usteer_snr_to_signal(si->node, min_snr); ret = false; goto out; - } else if (!config.assoc_steering) { + } else if (!SSID_CFG(si->node->ssid, assoc_steering)) { goto out; } } @@ -311,10 +313,11 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) goto out; } - if (current_time - si->created < config.initial_connect_delay) { + uint32_t initial_connect_delay = SSID_CFG(si->node->ssid, initial_connect_delay); + if (current_time - si->created < initial_connect_delay) { ev.reason = UEV_REASON_CONNECT_DELAY; ev.threshold.cur = current_time - si->created; - ev.threshold.ref = config.initial_connect_delay; + ev.threshold.ref = initial_connect_delay; ret = false; goto out; } @@ -348,10 +351,10 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type) break; } - if (!ret && si->stats[type].blocked_cur >= config.max_retry_band) { + if (!ret && si->stats[type].blocked_cur >= SSID_CFG(si->node->ssid, max_retry_band)) { ev.reason = UEV_REASON_RETRY_EXCEEDED; ev.threshold.cur = si->stats[type].blocked_cur; - ev.threshold.ref = config.max_retry_band; + ev.threshold.ref = SSID_CFG(si->node->ssid, max_retry_band); } usteer_event(&ev); @@ -480,7 +483,7 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) case ROAM_TRIGGER_SCAN_DONE: /* Roaming time over, switch back to ROAM_TRIGGER_IDLE */ - if (si->roam_transition_start && current_time - si->roam_transition_start > config.roam_kick_delay) { + if (si->roam_transition_start && current_time - si->roam_transition_start > SSID_CFG(si->node->ssid, roam_kick_delay)) { si->roam_transition_start = 0; usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev); break; @@ -508,7 +511,7 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si) * placeholder reading that was never actually observed. */ if (si->aggressiveness >= 2 && !exploratory) { if (!si->kick_time) - si->kick_time = current_time + config.roam_kick_delay; + si->kick_time = current_time + SSID_CFG(si->node->ssid, roam_kick_delay); if (si->aggressiveness >= 3) disassoc_timer = (si->kick_time - current_time) / usteer_local_node_get_beacon_interval(ln); else @@ -548,11 +551,11 @@ bool usteer_policy_can_perform_roam(struct sta_info *si) return false; /* Skip on rejected transition */ - if (si->bss_transition_response.status_code && current_time - si->bss_transition_response.timestamp < config.steer_reject_timeout) + if (si->bss_transition_response.status_code && current_time - si->bss_transition_response.timestamp < SSID_CFG(si->node->ssid, steer_reject_timeout)) return false; /* Skip if transition accepted */ - if (!si->bss_transition_response.status_code && current_time - si->bss_transition_response.timestamp < config.roam_kick_delay) + if (!si->bss_transition_response.status_code && current_time - si->bss_transition_response.timestamp < SSID_CFG(si->node->ssid, roam_kick_delay)) return false; /* Skip on previous kick attempt */ diff --git a/remote.c b/remote.c index 7beadb0..607a396 100644 --- a/remote.c +++ b/remote.c @@ -34,6 +34,7 @@ #include "remote.h" #include "node.h" #include "known.h" +#include "ssid_config.h" static uint32_t local_id; static struct uloop_fd remote_fd; @@ -193,7 +194,7 @@ interface_add_station(struct usteer_remote_node *node, struct blob_attr *data) if (!local_si) continue; - if (current_time - local_si->last_connected < config.roam_process_timeout) { + if (current_time - local_si->last_connected < SSID_CFG(si->node->ssid, roam_process_timeout)) { node->node.roam_events.target++; break; } @@ -596,7 +597,7 @@ static void usteer_send_sta_info(struct sta_info *sta) blob_put_int32(&buf, APMSG_STA_SIGNAL, sta->signal); 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); + blob_put_int32(&buf, APMSG_STA_TIMEOUT, SSID_CFG(sta->node->ssid, local_sta_timeout) - seen); blob_nest_end(&buf, c); } diff --git a/ssid_config.c b/ssid_config.c index 8b1ed86..6c610e1 100644 --- a/ssid_config.c +++ b/ssid_config.c @@ -35,6 +35,7 @@ usteer_ssid_config_free(struct usteer_ssid_config *sc) { avl_delete(&ssid_configs, &sc->avl); free(sc->aggressiveness_mac_list); + free(sc->node_up_script); /* sc->avl.key (ssid_buf) is NOT a separate allocation - it was * carved out of the same calloc_a() block as sc itself, so freeing * sc below also releases it. Freeing it separately here corrupts @@ -43,57 +44,84 @@ usteer_ssid_config_free(struct usteer_ssid_config *sc) free(sc); } +/* Enum/policy/set/get order mirrors struct usteer_config's declaration order. */ enum { SCFG_SSID, + SCFG_STA_BLOCK_TIMEOUT, + SCFG_LOCAL_STA_TIMEOUT, + SCFG_MAX_RETRY_BAND, + SCFG_SEEN_POLICY_TIMEOUT, + SCFG_ASSOC_STEERING, + SCFG_PROBE_STEERING, + SCFG_MAX_NEIGHBOR_REPORTS, + SCFG_BAND_STEERING_THRESHOLD, + SCFG_LOAD_BALANCING_THRESHOLD, + SCFG_AGGRESSIVENESS, + SCFG_AGGRESSIVENESS_MAC_LIST, SCFG_MIN_SNR, SCFG_MIN_SNR_KICK_DELAY, SCFG_MIN_CONNECT_SNR, SCFG_SIGNAL_DIFF_THRESHOLD, + SCFG_STEER_REJECT_TIMEOUT, SCFG_ROAM_SCAN_SNR, + SCFG_ROAM_PROCESS_TIMEOUT, SCFG_ROAM_SCAN_TRIES, SCFG_ROAM_SCAN_TIMEOUT, SCFG_ROAM_SCAN_INTERVAL, SCFG_ROAM_TRIGGER_SNR, SCFG_ROAM_TRIGGER_INTERVAL, - SCFG_AGGRESSIVENESS, - SCFG_AGGRESSIVENESS_MAC_LIST, - SCFG_BAND_STEERING_THRESHOLD, + SCFG_ROAM_KICK_DELAY, SCFG_BAND_STEERING_ENABLED, SCFG_BAND_STEERING_INTERVAL, SCFG_BAND_STEERING_MIN_SNR, SCFG_BAND_STEERING_SIGNAL_THRESHOLD, + SCFG_INITIAL_CONNECT_DELAY, SCFG_LOAD_KICK_ENABLED, SCFG_LOAD_KICK_THRESHOLD, SCFG_LOAD_KICK_DELAY, SCFG_LOAD_KICK_MIN_CLIENTS, SCFG_LOAD_KICK_REASON_CODE, + SCFG_NODE_UP_SCRIPT, __SCFG_MAX }; static const struct blobmsg_policy ssid_cfg_policy[__SCFG_MAX] = { [SCFG_SSID] = { "ssid", BLOBMSG_TYPE_STRING }, + [SCFG_STA_BLOCK_TIMEOUT] = { "sta_block_timeout", BLOBMSG_TYPE_INT32 }, + [SCFG_LOCAL_STA_TIMEOUT] = { "local_sta_timeout", BLOBMSG_TYPE_INT32 }, + [SCFG_MAX_RETRY_BAND] = { "max_retry_band", BLOBMSG_TYPE_INT32 }, + [SCFG_SEEN_POLICY_TIMEOUT] = { "seen_policy_timeout", BLOBMSG_TYPE_INT32 }, + [SCFG_ASSOC_STEERING] = { "assoc_steering", BLOBMSG_TYPE_BOOL }, + [SCFG_PROBE_STEERING] = { "probe_steering", BLOBMSG_TYPE_BOOL }, + [SCFG_MAX_NEIGHBOR_REPORTS] = { "max_neighbor_reports", BLOBMSG_TYPE_INT32 }, + [SCFG_BAND_STEERING_THRESHOLD] = { "band_steering_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_LOAD_BALANCING_THRESHOLD] = { "load_balancing_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_AGGRESSIVENESS] = { "aggressiveness", BLOBMSG_TYPE_INT32 }, + [SCFG_AGGRESSIVENESS_MAC_LIST] = { "aggressiveness_mac_list", BLOBMSG_TYPE_ARRAY }, [SCFG_MIN_SNR] = { "min_snr", BLOBMSG_TYPE_INT32 }, [SCFG_MIN_SNR_KICK_DELAY] = { "min_snr_kick_delay", BLOBMSG_TYPE_INT32 }, [SCFG_MIN_CONNECT_SNR] = { "min_connect_snr", BLOBMSG_TYPE_INT32 }, [SCFG_SIGNAL_DIFF_THRESHOLD] = { "signal_diff_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_STEER_REJECT_TIMEOUT] = { "steer_reject_timeout", BLOBMSG_TYPE_INT32 }, [SCFG_ROAM_SCAN_SNR] = { "roam_scan_snr", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_PROCESS_TIMEOUT] = { "roam_process_timeout", BLOBMSG_TYPE_INT32 }, [SCFG_ROAM_SCAN_TRIES] = { "roam_scan_tries", BLOBMSG_TYPE_INT32 }, [SCFG_ROAM_SCAN_TIMEOUT] = { "roam_scan_timeout", BLOBMSG_TYPE_INT32 }, [SCFG_ROAM_SCAN_INTERVAL] = { "roam_scan_interval", BLOBMSG_TYPE_INT32 }, [SCFG_ROAM_TRIGGER_SNR] = { "roam_trigger_snr", BLOBMSG_TYPE_INT32 }, [SCFG_ROAM_TRIGGER_INTERVAL] = { "roam_trigger_interval", BLOBMSG_TYPE_INT32 }, - [SCFG_AGGRESSIVENESS] = { "aggressiveness", BLOBMSG_TYPE_INT32 }, - [SCFG_AGGRESSIVENESS_MAC_LIST] = { "aggressiveness_mac_list", BLOBMSG_TYPE_ARRAY }, - [SCFG_BAND_STEERING_THRESHOLD] = { "band_steering_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_ROAM_KICK_DELAY] = { "roam_kick_delay", BLOBMSG_TYPE_INT32 }, [SCFG_BAND_STEERING_ENABLED] = { "band_steering_enabled", BLOBMSG_TYPE_BOOL }, [SCFG_BAND_STEERING_INTERVAL] = { "band_steering_interval", BLOBMSG_TYPE_INT32 }, [SCFG_BAND_STEERING_MIN_SNR] = { "band_steering_min_snr", BLOBMSG_TYPE_INT32 }, [SCFG_BAND_STEERING_SIGNAL_THRESHOLD] = { "band_steering_signal_threshold", BLOBMSG_TYPE_INT32 }, + [SCFG_INITIAL_CONNECT_DELAY] = { "initial_connect_delay", BLOBMSG_TYPE_INT32 }, [SCFG_LOAD_KICK_ENABLED] = { "load_kick_enabled", BLOBMSG_TYPE_BOOL }, [SCFG_LOAD_KICK_THRESHOLD] = { "load_kick_threshold", BLOBMSG_TYPE_INT32 }, [SCFG_LOAD_KICK_DELAY] = { "load_kick_delay", BLOBMSG_TYPE_INT32 }, [SCFG_LOAD_KICK_MIN_CLIENTS] = { "load_kick_min_clients", BLOBMSG_TYPE_INT32 }, [SCFG_LOAD_KICK_REASON_CODE] = { "load_kick_reason_code", BLOBMSG_TYPE_INT32 }, + [SCFG_NODE_UP_SCRIPT] = { "node_up_script", BLOBMSG_TYPE_STRING }, }; #define SCFG_INT(_sc, _tb, _field, _idx) \ @@ -136,28 +164,46 @@ void config_set_ssid_configs(struct blob_attr *data) strcpy(ssid_buf, blobmsg_get_string(tb[SCFG_SSID])); sc->avl.key = ssid_buf; + SCFG_INT(sc, tb, sta_block_timeout, SCFG_STA_BLOCK_TIMEOUT); + SCFG_INT(sc, tb, local_sta_timeout, SCFG_LOCAL_STA_TIMEOUT); + SCFG_INT(sc, tb, max_retry_band, SCFG_MAX_RETRY_BAND); + SCFG_INT(sc, tb, seen_policy_timeout, SCFG_SEEN_POLICY_TIMEOUT); + SCFG_BOOL(sc, tb, assoc_steering, SCFG_ASSOC_STEERING); + SCFG_BOOL(sc, tb, probe_steering, SCFG_PROBE_STEERING); + SCFG_INT(sc, tb, max_neighbor_reports, SCFG_MAX_NEIGHBOR_REPORTS); + SCFG_INT(sc, tb, band_steering_threshold, SCFG_BAND_STEERING_THRESHOLD); + SCFG_INT(sc, tb, load_balancing_threshold, SCFG_LOAD_BALANCING_THRESHOLD); + SCFG_INT(sc, tb, aggressiveness, SCFG_AGGRESSIVENESS); + SCFG_LIST(sc, tb, aggressiveness_mac_list, SCFG_AGGRESSIVENESS_MAC_LIST); SCFG_SINT(sc, tb, min_snr, SCFG_MIN_SNR); SCFG_INT(sc, tb, min_snr_kick_delay, SCFG_MIN_SNR_KICK_DELAY); SCFG_SINT(sc, tb, min_connect_snr, SCFG_MIN_CONNECT_SNR); SCFG_INT(sc, tb, signal_diff_threshold, SCFG_SIGNAL_DIFF_THRESHOLD); + SCFG_INT(sc, tb, steer_reject_timeout, SCFG_STEER_REJECT_TIMEOUT); SCFG_SINT(sc, tb, roam_scan_snr, SCFG_ROAM_SCAN_SNR); + SCFG_INT(sc, tb, roam_process_timeout, SCFG_ROAM_PROCESS_TIMEOUT); SCFG_INT(sc, tb, roam_scan_tries, SCFG_ROAM_SCAN_TRIES); SCFG_INT(sc, tb, roam_scan_timeout, SCFG_ROAM_SCAN_TIMEOUT); SCFG_INT(sc, tb, roam_scan_interval, SCFG_ROAM_SCAN_INTERVAL); SCFG_SINT(sc, tb, roam_trigger_snr, SCFG_ROAM_TRIGGER_SNR); SCFG_INT(sc, tb, roam_trigger_interval, SCFG_ROAM_TRIGGER_INTERVAL); - SCFG_INT(sc, tb, aggressiveness, SCFG_AGGRESSIVENESS); - SCFG_LIST(sc, tb, aggressiveness_mac_list, SCFG_AGGRESSIVENESS_MAC_LIST); - SCFG_INT(sc, tb, band_steering_threshold, SCFG_BAND_STEERING_THRESHOLD); + SCFG_INT(sc, tb, roam_kick_delay, SCFG_ROAM_KICK_DELAY); SCFG_BOOL(sc, tb, band_steering_enabled, SCFG_BAND_STEERING_ENABLED); SCFG_INT(sc, tb, band_steering_interval, SCFG_BAND_STEERING_INTERVAL); SCFG_SINT(sc, tb, band_steering_min_snr, SCFG_BAND_STEERING_MIN_SNR); SCFG_INT(sc, tb, band_steering_signal_threshold, SCFG_BAND_STEERING_SIGNAL_THRESHOLD); + SCFG_INT(sc, tb, initial_connect_delay, SCFG_INITIAL_CONNECT_DELAY); SCFG_BOOL(sc, tb, load_kick_enabled, SCFG_LOAD_KICK_ENABLED); SCFG_INT(sc, tb, load_kick_threshold, SCFG_LOAD_KICK_THRESHOLD); SCFG_INT(sc, tb, load_kick_delay, SCFG_LOAD_KICK_DELAY); SCFG_INT(sc, tb, load_kick_min_clients, SCFG_LOAD_KICK_MIN_CLIENTS); SCFG_INT(sc, tb, load_kick_reason_code, SCFG_LOAD_KICK_REASON_CODE); + /* No global fallback copy here (unlike SCFG_LIST) - the real + * global value isn't config.node_up_script (dead field), so + * leave unset overrides NULL and let call sites fall back to + * the actual global themselves. */ + if (tb[SCFG_NODE_UP_SCRIPT]) + sc->node_up_script = strdup(blobmsg_get_string(tb[SCFG_NODE_UP_SCRIPT])); avl_insert(&ssid_configs, &sc->avl); } @@ -172,29 +218,43 @@ void config_get_ssid_configs(struct blob_buf *buf) avl_for_each_element(&ssid_configs, sc, avl) { e = blobmsg_open_table(buf, NULL); blobmsg_add_string(buf, "ssid", sc->avl.key); + blobmsg_add_u32(buf, "sta_block_timeout", sc->sta_block_timeout); + blobmsg_add_u32(buf, "local_sta_timeout", sc->local_sta_timeout); + blobmsg_add_u32(buf, "max_retry_band", sc->max_retry_band); + blobmsg_add_u32(buf, "seen_policy_timeout", sc->seen_policy_timeout); + blobmsg_add_u8(buf, "assoc_steering", sc->assoc_steering); + blobmsg_add_u8(buf, "probe_steering", sc->probe_steering); + blobmsg_add_u32(buf, "max_neighbor_reports", sc->max_neighbor_reports); + blobmsg_add_u32(buf, "band_steering_threshold", sc->band_steering_threshold); + blobmsg_add_u32(buf, "load_balancing_threshold", sc->load_balancing_threshold); + blobmsg_add_u32(buf, "aggressiveness", sc->aggressiveness); + if (sc->aggressiveness_mac_list) + blobmsg_add_blob(buf, sc->aggressiveness_mac_list); blobmsg_add_u32(buf, "min_snr", sc->min_snr); blobmsg_add_u32(buf, "min_snr_kick_delay", sc->min_snr_kick_delay); blobmsg_add_u32(buf, "min_connect_snr", sc->min_connect_snr); blobmsg_add_u32(buf, "signal_diff_threshold", sc->signal_diff_threshold); + blobmsg_add_u32(buf, "steer_reject_timeout", sc->steer_reject_timeout); blobmsg_add_u32(buf, "roam_scan_snr", sc->roam_scan_snr); + blobmsg_add_u32(buf, "roam_process_timeout", sc->roam_process_timeout); blobmsg_add_u32(buf, "roam_scan_tries", sc->roam_scan_tries); blobmsg_add_u32(buf, "roam_scan_timeout", sc->roam_scan_timeout); blobmsg_add_u32(buf, "roam_scan_interval", sc->roam_scan_interval); blobmsg_add_u32(buf, "roam_trigger_snr", sc->roam_trigger_snr); blobmsg_add_u32(buf, "roam_trigger_interval", sc->roam_trigger_interval); - blobmsg_add_u32(buf, "aggressiveness", sc->aggressiveness); - if (sc->aggressiveness_mac_list) - blobmsg_add_blob(buf, sc->aggressiveness_mac_list); - blobmsg_add_u32(buf, "band_steering_threshold", sc->band_steering_threshold); + blobmsg_add_u32(buf, "roam_kick_delay", sc->roam_kick_delay); blobmsg_add_u8(buf, "band_steering_enabled", sc->band_steering_enabled); blobmsg_add_u32(buf, "band_steering_interval", sc->band_steering_interval); blobmsg_add_u32(buf, "band_steering_min_snr", sc->band_steering_min_snr); blobmsg_add_u32(buf, "band_steering_signal_threshold", sc->band_steering_signal_threshold); + blobmsg_add_u32(buf, "initial_connect_delay", sc->initial_connect_delay); blobmsg_add_u8(buf, "load_kick_enabled", sc->load_kick_enabled); blobmsg_add_u32(buf, "load_kick_threshold", sc->load_kick_threshold); blobmsg_add_u32(buf, "load_kick_delay", sc->load_kick_delay); blobmsg_add_u32(buf, "load_kick_min_clients", sc->load_kick_min_clients); blobmsg_add_u32(buf, "load_kick_reason_code", sc->load_kick_reason_code); + if (sc->node_up_script) + blobmsg_add_string(buf, "node_up_script", sc->node_up_script); blobmsg_close_table(buf, e); } blobmsg_close_array(buf, c); diff --git a/ssid_config.h b/ssid_config.h index 913371f..fbb42d9 100644 --- a/ssid_config.h +++ b/ssid_config.h @@ -35,12 +35,34 @@ struct usteer_ssid_config { struct avl_node avl; /* key: heap-allocated copy of the SSID string */ + /* Field order mirrors struct usteer_config's declaration order. */ + uint32_t sta_block_timeout; + uint32_t local_sta_timeout; + + uint32_t max_retry_band; + uint32_t seen_policy_timeout; + + bool assoc_steering; + bool probe_steering; + + uint32_t max_neighbor_reports; + + uint32_t band_steering_threshold; + uint32_t load_balancing_threshold; + + uint32_t aggressiveness; + struct blob_attr *aggressiveness_mac_list; + int32_t min_snr; uint32_t min_snr_kick_delay; int32_t min_connect_snr; uint32_t signal_diff_threshold; + uint32_t steer_reject_timeout; + int32_t roam_scan_snr; + uint32_t roam_process_timeout; + uint32_t roam_scan_tries; uint32_t roam_scan_timeout; uint32_t roam_scan_interval; @@ -48,20 +70,29 @@ struct usteer_ssid_config { int32_t roam_trigger_snr; uint32_t roam_trigger_interval; - uint32_t aggressiveness; - struct blob_attr *aggressiveness_mac_list; + uint32_t roam_kick_delay; - uint32_t band_steering_threshold; bool band_steering_enabled; uint32_t band_steering_interval; int32_t band_steering_min_snr; uint32_t band_steering_signal_threshold; + uint32_t initial_connect_delay; + bool load_kick_enabled; uint32_t load_kick_threshold; uint32_t load_kick_delay; uint32_t load_kick_min_clients; uint32_t load_kick_reason_code; + + /* NULL if unset - unlike every other field here, the global + * fallback for this one is NOT config.node_up_script (that struct + * field is dead; the real global value is a private static in + * local_node.c, set via config_set_node_up_script()), so callers + * can't use the SSID_CFG() macro for it and must fall back to that + * global manually - see usteer_local_node_up_script_run() call + * sites. */ + char *node_up_script; }; extern struct avl_tree ssid_configs; diff --git a/sta.c b/sta.c index abb55ff..b2161f1 100644 --- a/sta.c +++ b/sta.c @@ -180,7 +180,7 @@ void usteer_sta_disconnected(struct sta_info *si) si->connected = STA_NOT_CONNECTED; si->kick_time = 0; si->connected_since = 0; - usteer_sta_info_update_timeout(si, config.local_sta_timeout); + usteer_sta_info_update_timeout(si, SSID_CFG(si->node->ssid, local_sta_timeout)); } void @@ -217,7 +217,7 @@ usteer_sta_info_update(struct sta_info *si, int signal, bool avg) else si->sta->seen_5ghz = 1; - usteer_sta_info_update_timeout(si, config.local_sta_timeout); + usteer_sta_info_update_timeout(si, SSID_CFG(si->node->ssid, local_sta_timeout)); } bool @@ -239,7 +239,7 @@ usteer_handle_sta_event(struct usteer_node *node, const uint8_t *addr, si->stats[type].requests++; diff = si->stats[type].blocked_last_time - current_time; - if (diff > config.sta_block_timeout) + if (diff > SSID_CFG(node->ssid, sta_block_timeout)) si->stats[type].blocked_cur = 0; ret = usteer_check_request(si, type); diff --git a/ubus.c b/ubus.c index 5275677..f9fcf89 100644 --- a/ubus.c +++ b/ubus.c @@ -698,10 +698,11 @@ usteer_ubus_disassoc_add_neighbors(struct sta_info *si) struct usteer_node *node, *last_remote_neighbor = NULL; int i = 0; void *c; + uint32_t max_neighbor_reports = SSID_CFG(si->node->ssid, max_neighbor_reports); c = blobmsg_open_array(&b, "neighbors"); for_each_local_node(node) { - if (i >= config.max_neighbor_reports) + if (i >= max_neighbor_reports) break; if (si->node == node) continue; @@ -709,7 +710,7 @@ usteer_ubus_disassoc_add_neighbors(struct sta_info *si) i++; } - while (i < config.max_neighbor_reports) { + while (i < max_neighbor_reports) { node = usteer_node_get_next_neighbor(si->node, last_remote_neighbor); if (!node) { /* No more nodes available */ From 50cecfc773584e5582e3c0f2bf0df967151615df Mon Sep 17 00:00:00 2001 From: Grzegorz Kaczor Date: Wed, 29 Jul 2026 21:16:04 +0200 Subject: [PATCH 5/5] Convert local_sta_update to a per-SSID override local_sta_update already re-arms its timeout individually per local node (one struct usteer_local_node per SSID interface), so unlike remote_update_interval/remote_node_timeout (a single daemon-wide sync timer and peer table), it was already structurally per-SSID - it just read the same global value everywhere. Add it to usteer_ssid_config following struct usteer_config's declaration order, and update its five call sites (band_steering.c, local_node.c, policy.c) to consult the station's own SSID via SSID_CFG(). Signed-off-by: Grzegorz Kaczor --- band_steering.c | 2 +- local_node.c | 5 +++-- openwrt/usteer/files/etc/init.d/usteer | 2 +- policy.c | 4 ++-- ssid_config.c | 4 ++++ ssid_config.h | 1 + 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/band_steering.c b/band_steering.c index 519f6b9..79ac881 100644 --- a/band_steering.c +++ b/band_steering.c @@ -86,7 +86,7 @@ void usteer_band_steering_perform_steer(struct usteer_local_node *ln) { bool band_steering_enabled = SSID_CFG(ln->node.ssid, band_steering_enabled); uint32_t band_steering_interval = SSID_CFG(ln->node.ssid, band_steering_interval); - unsigned int min_count = DIV_ROUND_UP(band_steering_interval, config.local_sta_update); + unsigned int min_count = DIV_ROUND_UP(band_steering_interval, SSID_CFG(ln->node.ssid, local_sta_update)); struct sta_info *si; uint32_t disassoc_timer; uint32_t validity_period; diff --git a/local_node.c b/local_node.c index 9b70eb3..2b0411d 100644 --- a/local_node.c +++ b/local_node.c @@ -682,12 +682,13 @@ usteer_local_node_state_next(struct uloop_timeout *timeout) static void usteer_local_node_request_link_measurement(struct usteer_local_node *ln) { - unsigned int min_count = DIV_ROUND_UP(config.link_measurement_interval, config.local_sta_update); struct usteer_node *node; struct sta_info *si; node = &ln->node; + unsigned int min_count = DIV_ROUND_UP(config.link_measurement_interval, SSID_CFG(node->ssid, local_sta_update)); + if (ln->link_measurement_tries < min_count) { ln->link_measurement_tries++; return; @@ -729,7 +730,7 @@ usteer_local_node_update(struct uloop_timeout *timeout) usteer_band_steering_perform_steer(ln); usteer_local_node_request_link_measurement(ln); - uloop_timeout_set(timeout, config.local_sta_update); + uloop_timeout_set(timeout, SSID_CFG(node->ssid, local_sta_update)); } static void diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer index 1504507..32b9785 100755 --- a/openwrt/usteer/files/etc/init.d/usteer +++ b/openwrt/usteer/files/etc/init.d/usteer @@ -116,7 +116,7 @@ uci_usteer_ssid() { uci_option_to_json_string "$cfg" node_up_script for opt in \ - sta_block_timeout local_sta_timeout \ + sta_block_timeout local_sta_timeout local_sta_update \ max_retry_band seen_policy_timeout \ max_neighbor_reports \ load_balancing_threshold \ diff --git a/policy.c b/policy.c index f83eb12..de6af95 100644 --- a/policy.c +++ b/policy.c @@ -619,7 +619,7 @@ static void usteer_local_node_snr_kick(struct usteer_local_node *ln) { int32_t min_snr = SSID_CFG(ln->node.ssid, min_snr); - unsigned int min_count = DIV_ROUND_UP(SSID_CFG(ln->node.ssid, min_snr_kick_delay), config.local_sta_update); + unsigned int min_count = DIV_ROUND_UP(SSID_CFG(ln->node.ssid, min_snr_kick_delay), SSID_CFG(ln->node.ssid, local_sta_update)); struct uevent ev = { .node_local = &ln->node, }; @@ -670,7 +670,7 @@ usteer_local_node_load_kick(struct usteer_local_node *ln) uint32_t load_kick_threshold = SSID_CFG(node->ssid, load_kick_threshold); uint32_t load_kick_delay = SSID_CFG(node->ssid, load_kick_delay); uint32_t load_kick_min_clients = SSID_CFG(node->ssid, load_kick_min_clients); - unsigned int min_count = DIV_ROUND_UP(load_kick_delay, config.local_sta_update); + unsigned int min_count = DIV_ROUND_UP(load_kick_delay, SSID_CFG(node->ssid, local_sta_update)); if (!load_kick_enabled || !load_kick_threshold || !load_kick_delay) return; diff --git a/ssid_config.c b/ssid_config.c index 6c610e1..6a57acf 100644 --- a/ssid_config.c +++ b/ssid_config.c @@ -49,6 +49,7 @@ enum { SCFG_SSID, SCFG_STA_BLOCK_TIMEOUT, SCFG_LOCAL_STA_TIMEOUT, + SCFG_LOCAL_STA_UPDATE, SCFG_MAX_RETRY_BAND, SCFG_SEEN_POLICY_TIMEOUT, SCFG_ASSOC_STEERING, @@ -89,6 +90,7 @@ static const struct blobmsg_policy ssid_cfg_policy[__SCFG_MAX] = { [SCFG_SSID] = { "ssid", BLOBMSG_TYPE_STRING }, [SCFG_STA_BLOCK_TIMEOUT] = { "sta_block_timeout", BLOBMSG_TYPE_INT32 }, [SCFG_LOCAL_STA_TIMEOUT] = { "local_sta_timeout", BLOBMSG_TYPE_INT32 }, + [SCFG_LOCAL_STA_UPDATE] = { "local_sta_update", BLOBMSG_TYPE_INT32 }, [SCFG_MAX_RETRY_BAND] = { "max_retry_band", BLOBMSG_TYPE_INT32 }, [SCFG_SEEN_POLICY_TIMEOUT] = { "seen_policy_timeout", BLOBMSG_TYPE_INT32 }, [SCFG_ASSOC_STEERING] = { "assoc_steering", BLOBMSG_TYPE_BOOL }, @@ -166,6 +168,7 @@ void config_set_ssid_configs(struct blob_attr *data) SCFG_INT(sc, tb, sta_block_timeout, SCFG_STA_BLOCK_TIMEOUT); SCFG_INT(sc, tb, local_sta_timeout, SCFG_LOCAL_STA_TIMEOUT); + SCFG_INT(sc, tb, local_sta_update, SCFG_LOCAL_STA_UPDATE); SCFG_INT(sc, tb, max_retry_band, SCFG_MAX_RETRY_BAND); SCFG_INT(sc, tb, seen_policy_timeout, SCFG_SEEN_POLICY_TIMEOUT); SCFG_BOOL(sc, tb, assoc_steering, SCFG_ASSOC_STEERING); @@ -220,6 +223,7 @@ void config_get_ssid_configs(struct blob_buf *buf) blobmsg_add_string(buf, "ssid", sc->avl.key); blobmsg_add_u32(buf, "sta_block_timeout", sc->sta_block_timeout); blobmsg_add_u32(buf, "local_sta_timeout", sc->local_sta_timeout); + blobmsg_add_u32(buf, "local_sta_update", sc->local_sta_update); blobmsg_add_u32(buf, "max_retry_band", sc->max_retry_band); blobmsg_add_u32(buf, "seen_policy_timeout", sc->seen_policy_timeout); blobmsg_add_u8(buf, "assoc_steering", sc->assoc_steering); diff --git a/ssid_config.h b/ssid_config.h index fbb42d9..103a5a7 100644 --- a/ssid_config.h +++ b/ssid_config.h @@ -38,6 +38,7 @@ struct usteer_ssid_config { /* Field order mirrors struct usteer_config's declaration order. */ uint32_t sta_block_timeout; uint32_t local_sta_timeout; + uint32_t local_sta_update; uint32_t max_retry_band; uint32_t seen_policy_timeout;