Files
usteer/policy.c
Stijn Tintel 73c424b9e4 usteer: add option for probe steering
Probe suppression is mostly useful in high density deployments, where
all APs responding to all client probes would result in all channels
being saturated. It effectively hides APs from clients, and can cause
problems in normal density deployments.

Add an option for it, so it can be disabled.

Signed-off-by: Stijn Tintel <stijn@linux-ipv6.be>
Acked-by: David Bauer <mail@david-bauer.net>
2022-06-28 03:27:13 +03:00

568 lines
13 KiB
C

/*
* 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.
*
* Copyright (C) 2020 embedd.ch
* Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
* Copyright (C) 2020 John Crispin <john@phrozen.org>
*/
#include "usteer.h"
#include "node.h"
#include "event.h"
static bool
below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new)
{
int n_assoc_cur = node_cur->n_assoc;
int n_assoc_new = node_new->n_assoc;
bool ref_5g = node_cur->freq > 4000;
bool node_5g = node_new->freq > 4000;
if (!config.load_balancing_threshold)
return false;
if (ref_5g && !node_5g)
n_assoc_new += config.band_steering_threshold;
else if (!ref_5g && node_5g)
n_assoc_cur += config.band_steering_threshold;
n_assoc_new += config.load_balancing_threshold;
return n_assoc_new <= n_assoc_cur;
}
static bool
better_signal_strength(int signal_cur, int signal_new)
{
const bool is_better = signal_new - signal_cur
> (int) config.signal_diff_threshold;
if (!config.signal_diff_threshold)
return false;
return is_better;
}
static bool
below_load_threshold(struct usteer_node *node)
{
return node->n_assoc >= config.load_kick_min_clients &&
node->load > config.load_kick_threshold;
}
static bool
has_better_load(struct usteer_node *node_cur, struct usteer_node *node_new)
{
return !below_load_threshold(node_cur) && below_load_threshold(node_new);
}
bool
usteer_policy_node_below_max_assoc(struct usteer_node *node)
{
return !node->max_assoc || node->n_assoc < node->max_assoc;
}
static bool
over_min_signal(struct usteer_node *node, int signal)
{
if (config.min_snr && signal < usteer_snr_to_signal(node, config.min_snr))
return false;
if (config.roam_trigger_snr && signal < usteer_snr_to_signal(node, config.roam_trigger_snr))
return false;
return true;
}
static uint32_t
is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new)
{
struct usteer_node *current_node = si_cur->node;
struct usteer_node *new_node = si_new->node;
int current_signal = si_cur->signal;
int new_signal = si_new->signal;
uint32_t reasons = 0;
if (!usteer_policy_node_below_max_assoc(new_node))
return 0;
if (!over_min_signal(new_node, new_signal))
return 0;
if (below_assoc_threshold(current_node, new_node) &&
!below_assoc_threshold(new_node, current_node))
reasons |= (1 << UEV_SELECT_REASON_NUM_ASSOC);
if (better_signal_strength(current_signal, new_signal))
reasons |= (1 << UEV_SELECT_REASON_SIGNAL);
if (has_better_load(current_node, new_node) &&
!has_better_load(current_node, new_node))
reasons |= (1 << UEV_SELECT_REASON_LOAD);
return reasons;
}
static struct sta_info *
find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t required_criteria, uint64_t max_age)
{
struct sta_info *si, *candidate = NULL;
struct sta *sta = si_ref->sta;
uint32_t reasons;
list_for_each_entry(si, &sta->nodes, list) {
if (si == si_ref)
continue;
if (current_time - si->seen > config.seen_policy_timeout)
continue;
if (strcmp(si->node->ssid, si_ref->node->ssid) != 0)
continue;
if (max_age && max_age < current_time - si->seen)
continue;
reasons = is_better_candidate(si_ref, si);
if (!reasons)
continue;
if (!(reasons & required_criteria))
continue;
if (ev) {
ev->si_other = si;
ev->select_reasons = reasons;
}
if (!candidate || si->signal > candidate->signal)
candidate = si;
}
return candidate;
}
int
usteer_snr_to_signal(struct usteer_node *node, int snr)
{
int noise = -95;
if (snr < 0)
return snr;
if (node->noise)
noise = node->noise;
return noise + snr;
}
bool
usteer_check_request(struct sta_info *si, enum usteer_event_type type)
{
struct uevent ev = {
.si_cur = si,
};
int min_signal;
bool ret = true;
if (type == EVENT_TYPE_PROBE && !config.probe_steering)
goto out;
if (type == EVENT_TYPE_AUTH)
goto out;
if (type == EVENT_TYPE_ASSOC) {
/* Check if assoc request has lower signal than min_signal.
* If this is the case, block assoc even when assoc steering is enabled.
*
* 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)) {
ev.reason = UEV_REASON_LOW_SIGNAL;
ev.threshold.cur = si->signal;
ev.threshold.ref = usteer_snr_to_signal(si->node, config.min_snr);
ret = false;
goto out;
} else if (!config.assoc_steering) {
goto out;
}
}
min_signal = usteer_snr_to_signal(si->node, config.min_connect_snr);
if (si->signal < min_signal) {
ev.reason = UEV_REASON_LOW_SIGNAL;
ev.threshold.cur = si->signal;
ev.threshold.ref = min_signal;
ret = false;
goto out;
}
if (current_time - si->created < config.initial_connect_delay) {
ev.reason = UEV_REASON_CONNECT_DELAY;
ev.threshold.cur = current_time - si->created;
ev.threshold.ref = config.initial_connect_delay;
ret = false;
goto out;
}
if (!find_better_candidate(si, &ev, UEV_SELECT_REASON_ALL, 0))
goto out;
ev.reason = UEV_REASON_BETTER_CANDIDATE;
ev.node_cur = si->node;
ret = false;
out:
switch (type) {
case EVENT_TYPE_PROBE:
ev.type = ret ? UEV_PROBE_REQ_ACCEPT : UEV_PROBE_REQ_DENY;
break;
case EVENT_TYPE_ASSOC:
ev.type = ret ? UEV_ASSOC_REQ_ACCEPT : UEV_ASSOC_REQ_DENY;
break;
case EVENT_TYPE_AUTH:
ev.type = ret ? UEV_AUTH_REQ_ACCEPT : UEV_AUTH_REQ_DENY;
break;
default:
break;
}
if (!ret && si->stats[type].blocked_cur >= config.max_retry_band) {
ev.reason = UEV_REASON_RETRY_EXCEEDED;
ev.threshold.cur = si->stats[type].blocked_cur;
ev.threshold.ref = config.max_retry_band;
}
usteer_event(&ev);
return ret;
}
static bool
is_more_kickable(struct sta_info *si_cur, struct sta_info *si_new)
{
if (!si_cur)
return true;
if (si_new->kick_count > si_cur->kick_count)
return false;
return si_cur->signal > si_new->signal;
}
static void
usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state,
struct uevent *ev)
{
si->roam_event = current_time;
if (si->roam_state == state) {
if (si->roam_state == ROAM_TRIGGER_IDLE) {
si->roam_tries = 0;
return;
}
si->roam_tries++;
} else {
si->roam_tries = 0;
}
si->roam_state = state;
usteer_event(ev);
}
static void
usteer_roam_sm_start_scan(struct sta_info *si, struct uevent *ev)
{
/* 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) {
usteer_roam_set_state(si, ROAM_TRIGGER_SCAN, ev);
return;
}
/* We are currently in scan timeout / cooldown.
* Check if we are in ROAM_TRIGGER_IDLE state. Enter this state if not.
*/
if (si->roam_state == ROAM_TRIGGER_IDLE)
return;
/* Enter idle state */
usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
}
static struct sta_info *
usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum roam_trigger_state next_state)
{
uint64_t max_age = 2 * config.roam_scan_interval;
struct sta_info *candidate;
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);
if (candidate)
usteer_roam_set_state(si, next_state, ev);
return candidate;
}
static bool
usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si)
{
struct sta_info *candidate;
struct uevent ev = {
.si_cur = si,
};
switch (si->roam_state) {
case ROAM_TRIGGER_SCAN:
if (!si->roam_tries) {
si->roam_scan_start = current_time;
}
/* 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))
break;
/* Only scan every scan-interval */
if (current_time - si->roam_event < config.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) {
/* Prepare to kick client */
usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE, &ev);
} else {
/* Kick in scan timeout */
si->roam_scan_timeout_start = current_time;
usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
}
break;
}
/* Send beacon-request to client */
usteer_ubus_trigger_client_scan(si);
usteer_roam_sm_start_scan(si, &ev);
break;
case ROAM_TRIGGER_IDLE:
usteer_roam_sm_start_scan(si, &ev);
break;
case ROAM_TRIGGER_SCAN_DONE:
candidate = usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE);
/* Kick back in case no better node is found */
if (!candidate) {
usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
break;
}
usteer_ubus_bss_transition_request(si, 1, false, false, 100, candidate->node);
si->kick_time = current_time + config.roam_kick_delay;
usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
break;
}
return false;
}
static void
usteer_local_node_roam_check(struct usteer_local_node *ln, struct uevent *ev)
{
struct sta_info *si;
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;
else
return;
usteer_update_time();
min_signal = usteer_snr_to_signal(&ln->node, min_signal);
list_for_each_entry(si, &ln->node.sta_info, node_list) {
if (si->connected != STA_CONNECTED || si->signal >= min_signal ||
si->kick_time ||
(si->bss_transition_response.status_code && current_time - si->bss_transition_response.timestamp < config.steer_reject_timeout) ||
current_time - si->roam_kick < config.roam_trigger_interval) {
usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
continue;
}
/*
* If the state machine kicked a client, other clients should wait
* until the next turn
*/
if (usteer_roam_trigger_sm(ln, si))
return;
}
}
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);
struct uevent ev = {
.node_local = &ln->node,
};
struct sta_info *si;
int min_signal;
if (!config.min_snr)
return;
min_signal = usteer_snr_to_signal(&ln->node, config.min_snr);
ev.threshold.ref = min_signal;
list_for_each_entry(si, &ln->node.sta_info, node_list) {
if (si->connected != STA_CONNECTED)
continue;
if (si->signal >= min_signal) {
si->below_min_snr = 0;
continue;
} else {
si->below_min_snr++;
}
if (si->below_min_snr <= min_count)
continue;
si->kick_count++;
ev.type = UEV_SIGNAL_KICK;
ev.threshold.cur = si->signal;
ev.count = si->kick_count;
usteer_event(&ev);
usteer_ubus_kick_client(si);
return;
}
}
static void
usteer_local_node_load_kick(struct usteer_local_node *ln)
{
struct usteer_node *node = &ln->node;
struct sta_info *kick1 = NULL, *kick2 = NULL;
struct sta_info *candidate = NULL;
struct sta_info *si;
struct uevent ev = {
.node_local = &ln->node,
};
unsigned int min_count = DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update);
if (!config.load_kick_enabled || !config.load_kick_threshold ||
!config.load_kick_delay)
return;
if (node->load < config.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;
goto out;
}
if (++ln->load_thr_count <= min_count) {
if (ln->load_thr_count > 1)
return;
ev.type = UEV_LOAD_KICK_TRIGGER;
ev.threshold.cur = node->load;
ev.threshold.ref = config.load_kick_threshold;
goto out;
}
ln->load_thr_count = 0;
if (node->n_assoc < config.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;
goto out;
}
list_for_each_entry(si, &ln->node.sta_info, node_list) {
struct sta_info *tmp;
if (si->connected != STA_CONNECTED)
continue;
if (is_more_kickable(kick1, si))
kick1 = si;
tmp = find_better_candidate(si, NULL, (1 << UEV_SELECT_REASON_LOAD), 0);
if (!tmp)
continue;
if (is_more_kickable(kick2, si)) {
kick2 = si;
candidate = tmp;
}
}
if (!kick1) {
ev.type = UEV_LOAD_KICK_NO_CLIENT;
goto out;
}
if (kick2)
kick1 = kick2;
kick1->kick_count++;
ev.type = UEV_LOAD_KICK_CLIENT;
ev.si_cur = kick1;
ev.si_other = candidate;
ev.count = kick1->kick_count;
usteer_ubus_kick_client(kick1);
out:
usteer_event(&ev);
}
static void
usteer_local_node_perform_kick(struct usteer_local_node *ln)
{
struct sta_info *si;
list_for_each_entry(si, &ln->node.sta_info, node_list) {
if (!si->kick_time || si->kick_time > current_time)
continue;
usteer_ubus_kick_client(si);
}
}
void
usteer_local_node_kick(struct usteer_local_node *ln)
{
struct uevent ev = {
.node_local = &ln->node,
};
usteer_local_node_perform_kick(ln);
usteer_local_node_snr_kick(ln);
usteer_local_node_load_kick(ln);
usteer_local_node_roam_check(ln, &ev);
}