diff --git a/README.md b/README.md index 502ac0d..3a94a25 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/band_steering.c b/band_steering.c index 9f28603..3a6ec0a 100644 --- a/band_steering.c +++ b/band_steering.c @@ -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) { diff --git a/local_node.c b/local_node.c index d90e4a5..6e935c5 100644 --- a/local_node.c +++ b/local_node.c @@ -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; } diff --git a/main.c b/main.c index bd6ae4b..b47e59c 100644 --- a/main.c +++ b/main.c @@ -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; diff --git a/openwrt/usteer/files/etc/config/usteer b/openwrt/usteer/files/etc/config/usteer index dc06b7c..943d3e2 100644 --- a/openwrt/usteer/files/etc/config/usteer +++ b/openwrt/usteer/files/etc/config/usteer @@ -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. diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer index 5c99bec..7367cf9 100755 --- a/openwrt/usteer/files/etc/init.d/usteer +++ b/openwrt/usteer/files/etc/init.d/usteer @@ -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 diff --git a/policy.c b/policy.c index 16c4363..429b1e2 100644 --- a/policy.c +++ b/policy.c @@ -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. * @@ -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; } diff --git a/ubus.c b/ubus.c index dca6f9a..074ad1a 100644 --- a/ubus.c +++ b/ubus.c @@ -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), \ diff --git a/usteer.h b/usteer.h index 407d02d..98ccfcf 100644 --- a/usteer.h +++ b/usteer.h @@ -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; @@ -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]; }; @@ -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);