Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Its goal is to optimize roaming behavior of wireless clients (STAs) in a ESS con
- Policy-based decisions for probe- / association- / authentication requests received from STAs
- Requesting clients to roam to a different BSS based on SNR / signal-level
- Channel-load based client steering to different BSS
- Same-AP down-steering of clients stuck on a weak upper band (6->5->2.4) when no better AP is available

## Installation

Expand Down
26 changes: 26 additions & 0 deletions band_steering.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@
#include "usteer.h"
#include "node.h"

/* Return a node of the SAME AP (local node) on the next-lower band (6->5->2.4).
* Used as a fallback when the roam state machine finds no measured better
* candidate: the same-AP lower band is guaranteed reachable and physically
* stronger than a weak upper band, even though its per-STA signal is unknown. */
struct usteer_node *usteer_band_downsteer_target(struct usteer_local_node *ln)
{
struct usteer_node *cur = &ln->node;
struct usteer_node *node, *best = NULL;

for_each_local_node(node) {
if (node == cur)
continue;
if (strcmp(cur->ssid, node->ssid) != 0)
continue;
if (node->freq >= cur->freq)
continue;
if (!usteer_policy_node_below_max_assoc(node))
continue;
/* Highest freq strictly below current => one band down. */
if (!best || node->freq > best->freq)
best = node;
}

return best;
}

void usteer_band_steering_sta_update(struct sta_info *si)
{
if (si->connected == STA_NOT_CONNECTED) {
Expand Down
12 changes: 7 additions & 5 deletions local_node.c
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,13 @@ 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) {
/* 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));
}
/* PR#16 (openwrt/usteer): KEEP the already-scheduled fallback kick even if
* the client REJECTS the BSS-TM (status_code != 0). Cancelling it here left
* stubborn clients (notably iPhones) stuck for many minutes on soft-steering
* retries and DEFEATED the forced band-downsteer (which schedules kick_time).
* Ask nicely via BSS-TM first; if the client refuses, kick_time still deauths
* it (roam_kick_delay / band_downsteer_disassoc), then assoc_min_snr keeps it
* off the weak upper band. */

return 0;
}
Expand Down
4 changes: 4 additions & 0 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ void usteer_init_defaults(void)

config.band_steering_interval = 30000;
config.band_steering_min_snr = -60;
config.band_downsteer_snr = 0;
config.band_downsteer_hold = 30 * 1000;
config.assoc_min_snr = 0;
config.band_downsteer_disassoc = 0;
config.band_steering_signal_threshold = 5;

config.link_measurement_interval = 30000;
Expand Down
38 changes: 38 additions & 0 deletions openwrt/usteer/files/etc/config/usteer
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,44 @@ config usteer
# steered to a higher frequency band
#option band_steering_min_snr -60

# Signal level (dBm) below which a client on an upper band (5/6 GHz) is
# down-steered to the next-lower band of the SAME AP (6->5->2.4), but only
# when the roaming policy has found no better measured candidate on another
# AP. Fixes clients (notably iOS) that cling to a weak 6 GHz signal while a
# stronger lower band of the same AP is available. A value of 0 disables it.
# Pair with band_downsteer_hold (below) for durability: min_snr-style kicking
# tends to fight the client's own band preference and cause an assoc/kick
# storm, whereas the directed BSS-TM plus a hold moves the client gently.
#option band_downsteer_snr 0

# Time (ms) after a down-steer during which the client is not allowed back
# up to a higher band than it was moved to. Time-based hysteresis that stops
# bouncing on a fluctuating signal. 0 disables the hold. NOTE: the hold state
# is per-AP (not synced between nodes), so it stops bounce-back to the SAME
# AP's upper band but not a jump to another AP's upper band. Use assoc_min_snr
# (below) to close that cross-AP gap.
#option band_downsteer_hold 30000

# Admission floor (dBm): refuse a client's association to an UPPER band
# (5/6 GHz) on ANY AP when its signal is below this value; 2.4 GHz is always
# exempt. This closes cross-AP bouncing onto weak 5/6 GHz that the per-AP
# band_downsteer_hold cannot: a client that is weak everywhere on the upper
# bands can only land on 2.4 GHz, so it settles instead of thrashing.
# Unlike min_snr this is ADMISSION-ONLY -- it never kicks a connected client,
# so it cannot cause the assoc/kick storm that min_snr does; and because
# 2.4 GHz is exempt the client always has a landing band (no lockout).
# Enforced independently of assoc_steering. 0 disables it.
#option assoc_min_snr 0

# Forced down-steer (ms): a plain down-steer is a BSS-TM suggestion that some
# clients (notably iOS) ignore, clinging to a dying upper band. If set, then
# this many ms after down-steering a client that is still on the upper band
# is kicked (deauth). Paired with assoc_min_snr this is safe: the kicked
# client cannot reassociate to the weak upper band, so it lands on 2.4/5 GHz
# and cannot bounce back -- no kick storm (the failure mode of blind min_snr).
# 0 = gentle (suggestion only). Typical: 8000 (give the client ~8s to comply).
#option band_downsteer_disassoc 0

# SNR difference that the signal must be better compared to signal was on connection to node.
# Avoids conflicts between roaming and band-steering policies.
# A value of 0 disables threshold.
Expand Down
2 changes: 1 addition & 1 deletion openwrt/usteer/files/etc/init.d/usteer
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ uci_usteer() {
roam_kick_delay roam_scan_tries roam_scan_timeout \
roam_scan_snr roam_scan_interval roam_trigger_snr roam_trigger_interval \
link_measurement_interval \
band_steering_interval band_steering_min_snr band_steering_signal_threshold \
band_steering_interval band_steering_min_snr band_downsteer_snr band_downsteer_hold assoc_min_snr band_downsteer_disassoc band_steering_signal_threshold \
load_kick_threshold load_kick_delay load_kick_min_clients \
load_kick_reason_code
do
Expand Down
55 changes: 55 additions & 0 deletions policy.c
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,34 @@ usteer_check_request(struct sta_info *si, enum usteer_event_type type)
if (type == EVENT_TYPE_AUTH)
goto out;

/* Down-steer hold-down: for band_downsteer_hold ms after a down-steer,
* refuse probe/assoc for a band higher than the one the client was moved
* to. Time-based hysteresis stops a client bouncing back up on brief signal
* peaks. */
if (config.band_downsteer_hold && si->sta->downsteer_hold_until &&
current_time < si->sta->downsteer_hold_until &&
si->node->freq > si->sta->downsteer_to_freq) {
ret = false;
goto out;
}

if (type == EVENT_TYPE_ASSOC) {
/* Band-aware admission floor (assoc_min_snr): refuse association to an
* upper band (> 2.4 GHz) when the client's signal is below the floor,
* regardless of assoc_steering. This closes cross-node bouncing onto
* weak 5/6 GHz on ANY AP. 2.4 GHz is exempt, so the client always has a
* landing band and cannot be locked out. Admission-only: a connected
* client is never kicked, so unlike min_snr this cannot cause a kick
* storm. */
if (config.assoc_min_snr && si->node->freq > 4000 &&
si->signal < usteer_snr_to_signal(si->node, config.assoc_min_snr)) {
ev.reason = UEV_REASON_LOW_SIGNAL;
ev.threshold.cur = si->signal;
ev.threshold.ref = usteer_snr_to_signal(si->node, config.assoc_min_snr);
ret = false;
goto out;
}

/* Check if assoc request has lower signal than min_signal.
* If this is the case, block assoc even when assoc steering is enabled.
*
Expand Down Expand Up @@ -374,6 +401,34 @@ usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si)
candidate = usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE);
/* Kick back in case no better node is found */
if (!candidate) {
/* No measured better route. Fall back to same-AP down-steering:
* if the client sits on an upper band (5/6 GHz) below
* band_downsteer_snr, push it to the next-lower band of the same
* AP. min_snr / min_connect_snr then refuse a bounce back up to a
* band below the threshold, so the move is durable. */
if (config.band_downsteer_snr && si->node->freq > 4000 &&
si->signal < usteer_snr_to_signal(si->node, config.band_downsteer_snr)) {
struct usteer_node *lower = usteer_band_downsteer_target(ln);
if (lower) {
uint32_t bi = usteer_local_node_get_beacon_interval(ln);
uint32_t vp = 10000 / bi;
uint32_t dtimer = config.band_downsteer_disassoc ?
config.band_downsteer_disassoc / bi : 0;
MSG(VERBOSE, "down-steer " MAC_ADDR_FMT " to %s (signal %d%s)\n",
MAC_ADDR_DATA(si->sta->addr), usteer_node_name(lower), si->signal,
config.band_downsteer_disassoc ? ", forced" : "");
usteer_ubus_bss_transition_request(si, 1, true, dtimer, true, vp, lower);
si->sta->downsteer_hold_until = current_time + config.band_downsteer_hold;
si->sta->downsteer_to_freq = lower->freq;
/* If the client ignores the BSS-TM (iOS often does) and is
* still on this upper band after band_downsteer_disassoc ms,
* kick it. assoc_min_snr then refuses reassoc to a weak upper
* band, so it lands on 2.4/5 and cannot bounce back -> no
* kick storm (unlike blind min_snr). */
if (config.band_downsteer_disassoc && !si->kick_time)
si->kick_time = current_time + config.band_downsteer_disassoc;
}
}
usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
break;
}
Expand Down
5 changes: 5 additions & 0 deletions ubus.c
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ struct cfg_item {
_cfg(U32, load_kick_reason_code), \
_cfg(U32, band_steering_interval), \
_cfg(I32, band_steering_min_snr), \
_cfg(U32, band_steering_signal_threshold), \
_cfg(I32, band_downsteer_snr), \
_cfg(U32, band_downsteer_hold), \
_cfg(I32, assoc_min_snr), \
_cfg(U32, band_downsteer_disassoc), \
_cfg(U32, link_measurement_interval), \
_cfg(ARRAY_CB, interfaces), \
_cfg(STRING_CB, node_up_script), \
Expand Down
13 changes: 13 additions & 0 deletions usteer.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ struct usteer_config {

uint32_t band_steering_interval;
int32_t band_steering_min_snr;
int32_t band_downsteer_snr;
uint32_t band_downsteer_hold;
/* Admission floor: refuse assoc to an upper band (>2.4 GHz) below this
* SNR, regardless of assoc_steering; 2.4 GHz exempt. Admission-only. */
int32_t assoc_min_snr;
/* Forced down-steer: ms after a down-steer BSS-TM to kick (deauth) a
* client still on the upper band. 0 = gentle (BSS-TM suggestion only). */
uint32_t band_downsteer_disassoc;
uint32_t band_steering_signal_threshold;

uint32_t link_measurement_interval;
Expand Down Expand Up @@ -303,6 +311,10 @@ struct sta {

uint32_t aggressiveness;

/* Down-steer hold: block upsteer above downsteer_to_freq until this time. */
uint64_t downsteer_hold_until;
uint32_t downsteer_to_freq;

uint8_t addr[6];
};

Expand Down Expand Up @@ -350,6 +362,7 @@ bool usteer_policy_can_perform_roam(struct sta_info *si);
void usteer_band_steering_perform_steer(struct usteer_local_node *ln);
void usteer_band_steering_sta_update(struct sta_info *si);
bool usteer_band_steering_is_target(struct usteer_local_node *ln, struct usteer_node *node);
struct usteer_node *usteer_band_downsteer_target(struct usteer_local_node *ln);

void usteer_ubus_init(struct ubus_context *ctx);
void usteer_ubus_kick_client(struct sta_info *si, uint32_t kick_reason_code);
Expand Down