Skip to content

Add known-station tracking and per-SSID configuration overrides - #22

Open
grzegorz914 wants to merge 7 commits into
openwrt:masterfrom
grzegorz914:master
Open

Add known-station tracking and per-SSID configuration overrides#22
grzegorz914 wants to merge 7 commits into
openwrt:masterfrom
grzegorz914:master

Conversation

@grzegorz914

@grzegorz914 grzegorz914 commented Jul 25, 2026

Copy link
Copy Markdown

Summary

  • Add known-station tracking with cold-start exploratory candidates: persist the best signal ever observed for a station on each node, synced between peers over the existing remote protocol, so stations that never probe/report RRM measurements once associated can still be found as roaming candidates. Candidates found only through this fallback are exploratory and are never used for a forced kick or an outright probe/assoc rejection, only a passive BSS-transition-request.
  • Add per-SSID configuration overrides via a new usteer_ssid UCI section type keyed by SSID, overriding signal thresholds, roam-scan/trigger tuning, band-steering (including a new band_steering_enabled toggle - band steering had no explicit on/off switch before, only an implicit one via a non-zero interval), load-kick, aggressiveness (plus its MAC-list), and a further batch of station/node-scoped policy fields (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, node_up_script) for stations on that SSID only. Any option left unset falls back to the global setting, so existing single-SSID configs are unaffected.
  • probe_steering was previously configurable only via a hardcoded default (config.probe_steering = 0 in usteer_init_defaults()) - it was never wired into the global UCI config policy, so there was no way to actually turn it on. It's now settable per-SSID.
  • ssid_config.h/.c's enum/policy/struct field order mirrors struct usteer_config's declaration order throughout, to keep the two structures easy to diff against each other as fields are added.

Test plan

  • Compiles cleanly for ipq806x/generic (arm_cortex-a15_neon-vfpv4) and ramips/mt7620 (mipsel_24kc)
  • Running on 4 production APs (mixed ipq806x/ramips) for this session; known-station cold-start behavior and per-SSID overrides verified live against real client roaming/steering events

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 <grzegorz914@icloud.com>
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 <grzegorz914@icloud.com>
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 <grzegorz914@icloud.com>
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 <grzegorz914@icloud.com>
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 <grzegorz914@icloud.com>
@Ramon00

Ramon00 commented Jul 30, 2026

Copy link
Copy Markdown

About the band steering enable/disable toggle, i do not really thing that is necessary, as the zero is clearly documented. The downside of adding this is that you can now have a double disable. This may break user expectations. Also Link measurement interval also disables with a zero. So it should be keep it as is, or change both.

@grzegorz914

Copy link
Copy Markdown
Author

Fair point, and I'd rather remove the ambiguity than duplicate the fix onto link_measurement_interval too.

The reason band_steering_enabled exists as its own field rather than reusing the interval=0 convention: band steering became a per-SSID setting in this same series, and the LuCI UI hides all four dependent fields (interval, min_snr, signal_threshold, and the toggle's own siblings) when it's off, driven off this one boolean - link_measurement_interval doesn't have that per-SSID/dependent-field-visibility angle, so the two aren't quite the same shape of problem.

To kill the double-disable you're flagging rather than spread it: band_steering_interval=0 no longer disables band steering on its own - band_steering_enabled is now the sole switch. Setting interval=0 while enabled=true means "steer every tick" rather than "disabled", same as it would for any other interval field once it's not also doing double duty as a sentinel.

band_steering_interval=0 used to disable band steering too, alongside
the band_steering_enabled toggle - two independent ways to turn the
same feature off that could disagree with each other. Drop the
interval=0 special case so the toggle is the only switch; interval=0
with the toggle on now means "steer every tick" like any other
interval field, not "disabled".

Signed-off-by: Grzegorz Kaczor <grzegorz914@icloud.com>
@Ramon00

Ramon00 commented Jul 31, 2026

Copy link
Copy Markdown

Setting interval=0 while enabled=true means "steer every tick" rather than "disabled"

That implementation may break current installations, i.e. if somebody has it set to zero then with this patch instead of disabling it will now do very aggressive band steering.

About the per SSID and known system tracking. What is exactly the point of that? To circumvent some bad behaving STAs? If so then maybe it is better to tune a few of the parameters based on client mac like the aggressiveness_mac_list? Maybe add a mac blacklist?
The thing is there really should not be a need to do per SSID tuning as everything is running on the same radios. So tuning for one SSID should also be good for another SSID

@grzegorz914

Copy link
Copy Markdown
Author

Fair - didn't think through the migration impact for anyone already relying on interval=0. Reverted band_steering_enabled entirely: back to interval=0 as the sole disable convention, same as link_measurement_interval, no new field.

On the per-SSID question in general: in my case (and I'd guess this is common) each SSID maps 1:1 to a radio, not to multiple SSIDs sharing one radio - Multimedia is a 5GHz-only SSID, another is 2.4GHz-only, one sits on a DFS channel and one doesn't. So it's less "tune SSID A differently from SSID B on the same hardware" and more "tune per-radio RF characteristics", which do genuinely warrant different min_snr/roam thresholds/timing even though the daemon config historically had no way to express that below the whole-instance level. A MAC-based blacklist/allowlist (extending aggressiveness_mac_list) solves a different problem - steering exceptions for specific misbehaving clients - not radio-specific tuning, so I don't think it replaces this, but happy to hear if you see it differently.

Reverts 458519e and the toggle itself, added a day earlier. Migration
risk: an existing deployment with band_steering_interval=0 (the
documented way to disable band steering) would silently switch from
"disabled" to "steer every tick" once band_steering_enabled became
the sole switch, instead of staying disabled. Back to interval=0 as
the only disable convention, same as link_measurement_interval.

Signed-off-by: Grzegorz Kaczor <grzegorz914@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants