usteer: Fix better candidate not being set in policy
[project/usteer.git] / policy.c
1 /*
2 * This program is free software; you can redistribute it and/or modify
3 * it under the terms of the GNU General Public License as published by
4 * the Free Software Foundation; either version 2 of the License.
5 *
6 * This program is distributed in the hope that it will be useful,
7 * but WITHOUT ANY WARRANTY; without even the implied warranty of
8 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 * GNU General Public License for more details.
10 *
11 * You should have received a copy of the GNU General Public License
12 * along with this program; if not, write to the Free Software
13 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
14 *
15 * Copyright (C) 2020 embedd.ch
16 * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
17 * Copyright (C) 2020 John Crispin <john@phrozen.org>
18 */
19
20 #include "usteer.h"
21 #include "node.h"
22 #include "event.h"
23
24 static bool
25 below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new)
26 {
27 int n_assoc_cur = node_cur->n_assoc;
28 int n_assoc_new = node_new->n_assoc;
29 bool ref_5g = node_cur->freq > 4000;
30 bool node_5g = node_new->freq > 4000;
31
32 if (ref_5g && !node_5g)
33 n_assoc_new += config.band_steering_threshold;
34 else if (!ref_5g && node_5g)
35 n_assoc_cur += config.band_steering_threshold;
36
37 n_assoc_new += config.load_balancing_threshold;
38
39 return n_assoc_new <= n_assoc_cur;
40 }
41
42 static bool
43 better_signal_strength(int signal_cur, int signal_new)
44 {
45 const bool is_better = signal_new - signal_cur
46 > (int) config.signal_diff_threshold;
47
48 if (!config.signal_diff_threshold)
49 return false;
50
51 return is_better;
52 }
53
54 static bool
55 below_load_threshold(struct usteer_node *node)
56 {
57 return node->n_assoc >= config.load_kick_min_clients &&
58 node->load > config.load_kick_threshold;
59 }
60
61 static bool
62 has_better_load(struct usteer_node *node_cur, struct usteer_node *node_new)
63 {
64 return !below_load_threshold(node_cur) && below_load_threshold(node_new);
65 }
66
67 bool
68 usteer_policy_node_below_max_assoc(struct usteer_node *node)
69 {
70 return !node->max_assoc || node->n_assoc < node->max_assoc;
71 }
72
73 static bool
74 over_min_signal(struct usteer_node *node, int signal)
75 {
76 if (config.min_snr && signal < usteer_snr_to_signal(node, config.min_snr))
77 return false;
78
79 if (config.roam_trigger_snr && signal < usteer_snr_to_signal(node, config.roam_trigger_snr))
80 return false;
81
82 return true;
83 }
84
85 static uint32_t
86 is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new)
87 {
88 struct usteer_node *current_node = si_cur->node;
89 struct usteer_node *new_node = si_new->node;
90 int current_signal = si_cur->signal;
91 int new_signal = si_new->signal;
92 uint32_t reasons = 0;
93
94 if (!usteer_policy_node_below_max_assoc(new_node))
95 return 0;
96
97 if (!over_min_signal(new_node, new_signal))
98 return 0;
99
100 if (below_assoc_threshold(current_node, new_node) &&
101 !below_assoc_threshold(new_node, current_node))
102 reasons |= (1 << UEV_SELECT_REASON_NUM_ASSOC);
103
104 if (better_signal_strength(current_signal, new_signal))
105 reasons |= (1 << UEV_SELECT_REASON_SIGNAL);
106
107 if (has_better_load(current_node, new_node) &&
108 !has_better_load(current_node, new_node))
109 reasons |= (1 << UEV_SELECT_REASON_LOAD);
110
111 return reasons;
112 }
113
114 static struct sta_info *
115 find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t required_criteria, uint64_t max_age)
116 {
117 struct sta_info *si, *candidate = NULL;
118 struct sta *sta = si_ref->sta;
119 uint32_t reasons;
120
121 list_for_each_entry(si, &sta->nodes, list) {
122 if (si == si_ref)
123 continue;
124
125 if (current_time - si->seen > config.seen_policy_timeout)
126 continue;
127
128 if (strcmp(si->node->ssid, si_ref->node->ssid) != 0)
129 continue;
130
131 if (max_age && max_age < current_time - si->seen)
132 continue;
133
134 reasons = is_better_candidate(si_ref, si);
135 if (!reasons)
136 continue;
137
138 if (!(reasons & required_criteria))
139 continue;
140
141 if (ev) {
142 ev->si_other = si;
143 ev->select_reasons = reasons;
144 }
145
146 if (!candidate || si->signal > candidate->signal)
147 candidate = si;
148 }
149
150 return candidate;
151 }
152
153 int
154 usteer_snr_to_signal(struct usteer_node *node, int snr)
155 {
156 int noise = -95;
157
158 if (snr < 0)
159 return snr;
160
161 if (node->noise)
162 noise = node->noise;
163
164 return noise + snr;
165 }
166
167 bool
168 usteer_check_request(struct sta_info *si, enum usteer_event_type type)
169 {
170 struct uevent ev = {
171 .si_cur = si,
172 };
173 int min_signal;
174 bool ret = true;
175
176 if (type == EVENT_TYPE_AUTH)
177 goto out;
178
179 if (type == EVENT_TYPE_ASSOC) {
180 /* Check if assoc request has lower signal than min_signal.
181 * If this is the case, block assoc even when assoc steering is enabled.
182 *
183 * Otherwise, the client potentially ends up in a assoc - kick loop.
184 */
185 if (config.min_snr && si->signal < usteer_snr_to_signal(si->node, config.min_snr)) {
186 ev.reason = UEV_REASON_LOW_SIGNAL;
187 ev.threshold.cur = si->signal;
188 ev.threshold.ref = usteer_snr_to_signal(si->node, config.min_snr);
189 ret = false;
190 goto out;
191 } else if (!config.assoc_steering) {
192 goto out;
193 }
194 }
195
196 min_signal = usteer_snr_to_signal(si->node, config.min_connect_snr);
197 if (si->signal < min_signal) {
198 ev.reason = UEV_REASON_LOW_SIGNAL;
199 ev.threshold.cur = si->signal;
200 ev.threshold.ref = min_signal;
201 ret = false;
202 goto out;
203 }
204
205 if (current_time - si->created < config.initial_connect_delay) {
206 ev.reason = UEV_REASON_CONNECT_DELAY;
207 ev.threshold.cur = current_time - si->created;
208 ev.threshold.ref = config.initial_connect_delay;
209 ret = false;
210 goto out;
211 }
212
213 if (!find_better_candidate(si, &ev, UEV_SELECT_REASON_ALL, 0))
214 goto out;
215
216 ev.reason = UEV_REASON_BETTER_CANDIDATE;
217 ev.node_cur = si->node;
218 ret = false;
219
220 out:
221 switch (type) {
222 case EVENT_TYPE_PROBE:
223 ev.type = UEV_PROBE_REQ_ACCEPT;
224 break;
225 case EVENT_TYPE_ASSOC:
226 ev.type = UEV_ASSOC_REQ_ACCEPT;
227 break;
228 case EVENT_TYPE_AUTH:
229 ev.type = UEV_AUTH_REQ_ACCEPT;
230 break;
231 default:
232 break;
233 }
234
235 if (!ret)
236 ev.type++;
237
238 if (!ret && si->stats[type].blocked_cur >= config.max_retry_band) {
239 ev.reason = UEV_REASON_RETRY_EXCEEDED;
240 ev.threshold.cur = si->stats[type].blocked_cur;
241 ev.threshold.ref = config.max_retry_band;
242 }
243 usteer_event(&ev);
244
245 return ret;
246 }
247
248 static bool
249 is_more_kickable(struct sta_info *si_cur, struct sta_info *si_new)
250 {
251 if (!si_cur)
252 return true;
253
254 if (si_new->kick_count > si_cur->kick_count)
255 return false;
256
257 return si_cur->signal > si_new->signal;
258 }
259
260 static void
261 usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state,
262 struct uevent *ev)
263 {
264 si->roam_event = current_time;
265
266 if (si->roam_state == state) {
267 if (si->roam_state == ROAM_TRIGGER_IDLE) {
268 si->roam_tries = 0;
269 return;
270 }
271
272 si->roam_tries++;
273 } else {
274 si->roam_tries = 0;
275 }
276
277 si->roam_state = state;
278 usteer_event(ev);
279 }
280
281 static void
282 usteer_roam_sm_start_scan(struct sta_info *si, struct uevent *ev)
283 {
284 /* Start scanning in case we are not timeout-constrained or timeout has expired */
285 if (!config.roam_scan_timeout ||
286 current_time > si->roam_scan_timeout_start + config.roam_scan_timeout) {
287 usteer_roam_set_state(si, ROAM_TRIGGER_SCAN, ev);
288 return;
289 }
290
291 /* We are currently in scan timeout / cooldown.
292 * Check if we are in ROAM_TRIGGER_IDLE state. Enter this state if not.
293 */
294 if (si->roam_state == ROAM_TRIGGER_IDLE)
295 return;
296
297 /* Enter idle state */
298 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
299 }
300
301 static struct sta_info *
302 usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum roam_trigger_state next_state)
303 {
304 uint64_t max_age = 2 * config.roam_scan_interval;
305 struct sta_info *candidate;
306
307 if (max_age > current_time - si->roam_scan_start)
308 max_age = current_time - si->roam_scan_start;
309
310 candidate = find_better_candidate(si, ev, (1 << UEV_SELECT_REASON_SIGNAL), max_age);
311 if (candidate)
312 usteer_roam_set_state(si, next_state, ev);
313
314 return candidate;
315 }
316
317 static bool
318 usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si)
319 {
320 struct sta_info *candidate;
321 struct uevent ev = {
322 .si_cur = si,
323 };
324
325 switch (si->roam_state) {
326 case ROAM_TRIGGER_SCAN:
327 if (!si->roam_tries) {
328 si->roam_scan_start = current_time;
329 }
330
331 /* Check if we've found a better node regardless of the scan-interval */
332 if (usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE))
333 break;
334
335 /* Only scan every scan-interval */
336 if (current_time - si->roam_event < config.roam_scan_interval)
337 break;
338
339 /* Check if no node was found within roam_scan_tries tries */
340 if (config.roam_scan_tries && si->roam_tries >= config.roam_scan_tries) {
341 if (!config.roam_scan_timeout) {
342 /* Prepare to kick client */
343 usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE, &ev);
344 } else {
345 /* Kick in scan timeout */
346 si->roam_scan_timeout_start = current_time;
347 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
348 }
349 break;
350 }
351
352 /* Send beacon-request to client */
353 usteer_ubus_trigger_client_scan(si);
354 usteer_roam_sm_start_scan(si, &ev);
355 break;
356
357 case ROAM_TRIGGER_IDLE:
358 usteer_roam_sm_start_scan(si, &ev);
359 break;
360
361 case ROAM_TRIGGER_SCAN_DONE:
362 candidate = usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE);
363 /* Kick back in case no better node is found */
364 if (!candidate) {
365 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
366 break;
367 }
368
369 usteer_ubus_bss_transition_request(si, 1, false, false, 100, candidate->node);
370 si->kick_time = current_time + config.roam_kick_delay;
371 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
372 break;
373 }
374
375 return false;
376 }
377
378 static void
379 usteer_local_node_roam_check(struct usteer_local_node *ln, struct uevent *ev)
380 {
381 struct sta_info *si;
382 int min_signal;
383
384 if (config.roam_scan_snr)
385 min_signal = config.roam_scan_snr;
386 else if (config.roam_trigger_snr)
387 min_signal = config.roam_trigger_snr;
388 else
389 return;
390
391 usteer_update_time();
392 min_signal = usteer_snr_to_signal(&ln->node, min_signal);
393
394 list_for_each_entry(si, &ln->node.sta_info, node_list) {
395 if (si->connected != STA_CONNECTED || si->signal >= min_signal ||
396 si->kick_time ||
397 (si->bss_transition_response.status_code && current_time - si->bss_transition_response.timestamp < config.steer_reject_timeout) ||
398 current_time - si->roam_kick < config.roam_trigger_interval) {
399 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
400 continue;
401 }
402
403 /*
404 * If the state machine kicked a client, other clients should wait
405 * until the next turn
406 */
407 if (usteer_roam_trigger_sm(ln, si))
408 return;
409 }
410 }
411
412 static void
413 usteer_local_node_snr_kick(struct usteer_local_node *ln)
414 {
415 unsigned int min_count = DIV_ROUND_UP(config.min_snr_kick_delay, config.local_sta_update);
416 struct uevent ev = {
417 .node_local = &ln->node,
418 };
419 struct sta_info *si;
420 int min_signal;
421
422 if (!config.min_snr)
423 return;
424
425 min_signal = usteer_snr_to_signal(&ln->node, config.min_snr);
426 ev.threshold.ref = min_signal;
427
428 list_for_each_entry(si, &ln->node.sta_info, node_list) {
429 if (si->connected != STA_CONNECTED)
430 continue;
431
432 if (si->signal >= min_signal) {
433 si->below_min_snr = 0;
434 continue;
435 } else {
436 si->below_min_snr++;
437 }
438
439 if (si->below_min_snr <= min_count)
440 continue;
441
442 si->kick_count++;
443
444 ev.type = UEV_SIGNAL_KICK;
445 ev.threshold.cur = si->signal;
446 ev.count = si->kick_count;
447 usteer_event(&ev);
448
449 usteer_ubus_kick_client(si);
450 return;
451 }
452 }
453
454 static void
455 usteer_local_node_load_kick(struct usteer_local_node *ln)
456 {
457 struct usteer_node *node = &ln->node;
458 struct sta_info *kick1 = NULL, *kick2 = NULL;
459 struct sta_info *candidate = NULL;
460 struct sta_info *si;
461 struct uevent ev = {
462 .node_local = &ln->node,
463 };
464 unsigned int min_count = DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update);
465
466 if (!config.load_kick_enabled || !config.load_kick_threshold ||
467 !config.load_kick_delay)
468 return;
469
470 if (node->load < config.load_kick_threshold) {
471 if (!ln->load_thr_count)
472 return;
473
474 ln->load_thr_count = 0;
475 ev.type = UEV_LOAD_KICK_RESET;
476 ev.threshold.cur = node->load;
477 ev.threshold.ref = config.load_kick_threshold;
478 goto out;
479 }
480
481 if (++ln->load_thr_count <= min_count) {
482 if (ln->load_thr_count > 1)
483 return;
484
485 ev.type = UEV_LOAD_KICK_TRIGGER;
486 ev.threshold.cur = node->load;
487 ev.threshold.ref = config.load_kick_threshold;
488 goto out;
489 }
490
491 ln->load_thr_count = 0;
492 if (node->n_assoc < config.load_kick_min_clients) {
493 ev.type = UEV_LOAD_KICK_MIN_CLIENTS;
494 ev.threshold.cur = node->n_assoc;
495 ev.threshold.ref = config.load_kick_min_clients;
496 goto out;
497 }
498
499 list_for_each_entry(si, &ln->node.sta_info, node_list) {
500 struct sta_info *tmp;
501
502 if (si->connected != STA_CONNECTED)
503 continue;
504
505 if (is_more_kickable(kick1, si))
506 kick1 = si;
507
508 tmp = find_better_candidate(si, NULL, (1 << UEV_SELECT_REASON_LOAD), 0);
509 if (!tmp)
510 continue;
511
512 if (is_more_kickable(kick2, si)) {
513 kick2 = si;
514 candidate = tmp;
515 }
516 }
517
518 if (!kick1) {
519 ev.type = UEV_LOAD_KICK_NO_CLIENT;
520 goto out;
521 }
522
523 if (kick2)
524 kick1 = kick2;
525
526 kick1->kick_count++;
527
528 ev.type = UEV_LOAD_KICK_CLIENT;
529 ev.si_cur = kick1;
530 ev.si_other = candidate;
531 ev.count = kick1->kick_count;
532
533 usteer_ubus_kick_client(kick1);
534
535 out:
536 usteer_event(&ev);
537 }
538
539 static void
540 usteer_local_node_perform_kick(struct usteer_local_node *ln)
541 {
542 struct sta_info *si;
543
544 list_for_each_entry(si, &ln->node.sta_info, node_list) {
545 if (!si->kick_time || si->kick_time > current_time)
546 continue;
547
548 usteer_ubus_kick_client(si);
549 }
550 }
551
552 void
553 usteer_local_node_kick(struct usteer_local_node *ln)
554 {
555 struct uevent ev = {
556 .node_local = &ln->node,
557 };
558
559 usteer_local_node_perform_kick(ln);
560
561 usteer_local_node_snr_kick(ln);
562 usteer_local_node_load_kick(ln);
563 usteer_local_node_roam_check(ln, &ev);
564 }