local-node: handle BSS transition queries
[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 sta_info *si_cur, struct sta_info *si_new)
26 {
27 int n_assoc_cur = si_cur->node->n_assoc;
28 int n_assoc_new = si_new->node->n_assoc;
29 bool ref_5g = si_cur->node->freq > 4000;
30 bool node_5g = si_new->node->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(struct sta_info *si_cur, struct sta_info *si_new)
44 {
45 const bool is_better = si_new->signal - si_cur->signal
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 sta_info *si)
56 {
57 return si->node->n_assoc >= config.load_kick_min_clients &&
58 si->node->load > config.load_kick_threshold;
59 }
60
61 static bool
62 has_better_load(struct sta_info *si_cur, struct sta_info *si_new)
63 {
64 return !below_load_threshold(si_cur) && below_load_threshold(si_new);
65 }
66
67 static bool
68 below_max_assoc(struct sta_info *si)
69 {
70 struct usteer_node *node = si->node;
71
72 return !node->max_assoc || node->n_assoc < node->max_assoc;
73 }
74
75 static bool
76 over_min_signal(struct sta_info *si)
77 {
78 if (config.min_snr && si->signal < usteer_snr_to_signal(si->node, config.min_snr))
79 return false;
80
81 if (config.roam_trigger_snr && si->signal < usteer_snr_to_signal(si->node, config.roam_trigger_snr))
82 return false;
83
84 return true;
85 }
86
87 static uint32_t
88 is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new)
89 {
90 uint32_t reasons = 0;
91
92 if (!below_max_assoc(si_new))
93 return 0;
94
95 if (!over_min_signal(si_new))
96 return 0;
97
98 if (below_assoc_threshold(si_cur, si_new) &&
99 !below_assoc_threshold(si_new, si_cur))
100 reasons |= (1 << UEV_SELECT_REASON_NUM_ASSOC);
101
102 if (better_signal_strength(si_cur, si_new))
103 reasons |= (1 << UEV_SELECT_REASON_SIGNAL);
104
105 if (has_better_load(si_cur, si_new) &&
106 !has_better_load(si_cur, si_new))
107 reasons |= (1 << UEV_SELECT_REASON_LOAD);
108
109 return reasons;
110 }
111
112 static struct sta_info *
113 find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t required_criteria, uint64_t max_age)
114 {
115 struct sta_info *si;
116 struct sta *sta = si_ref->sta;
117 uint32_t reasons;
118
119 list_for_each_entry(si, &sta->nodes, list) {
120 if (si == si_ref)
121 continue;
122
123 if (current_time - si->seen > config.seen_policy_timeout)
124 continue;
125
126 if (strcmp(si->node->ssid, si_ref->node->ssid) != 0)
127 continue;
128
129 if (max_age && max_age < current_time - si->seen)
130 continue;
131
132 reasons = is_better_candidate(si_ref, si);
133 if (!reasons)
134 continue;
135
136 if (!(reasons & required_criteria))
137 continue;
138
139 if (ev) {
140 ev->si_other = si;
141 ev->select_reasons = reasons;
142 }
143
144 return si;
145 }
146
147 return NULL;
148 }
149
150 int
151 usteer_snr_to_signal(struct usteer_node *node, int snr)
152 {
153 int noise = -95;
154
155 if (snr < 0)
156 return snr;
157
158 if (node->noise)
159 noise = node->noise;
160
161 return noise + snr;
162 }
163
164 bool
165 usteer_check_request(struct sta_info *si, enum usteer_event_type type)
166 {
167 struct uevent ev = {
168 .si_cur = si,
169 };
170 int min_signal;
171 bool ret = true;
172
173 if (type == EVENT_TYPE_AUTH)
174 goto out;
175
176 if (type == EVENT_TYPE_ASSOC) {
177 /* Check if assoc request has lower signal than min_signal.
178 * If this is the case, block assoc even when assoc steering is enabled.
179 *
180 * Otherwise, the client potentially ends up in a assoc - kick loop.
181 */
182 if (config.min_snr && si->signal < usteer_snr_to_signal(si->node, config.min_snr)) {
183 ev.reason = UEV_REASON_LOW_SIGNAL;
184 ev.threshold.cur = si->signal;
185 ev.threshold.ref = usteer_snr_to_signal(si->node, config.min_snr);
186 ret = false;
187 goto out;
188 } else if (!config.assoc_steering) {
189 goto out;
190 }
191 }
192
193 min_signal = usteer_snr_to_signal(si->node, config.min_connect_snr);
194 if (si->signal < min_signal) {
195 ev.reason = UEV_REASON_LOW_SIGNAL;
196 ev.threshold.cur = si->signal;
197 ev.threshold.ref = min_signal;
198 ret = false;
199 goto out;
200 }
201
202 if (current_time - si->created < config.initial_connect_delay) {
203 ev.reason = UEV_REASON_CONNECT_DELAY;
204 ev.threshold.cur = current_time - si->created;
205 ev.threshold.ref = config.initial_connect_delay;
206 ret = false;
207 goto out;
208 }
209
210 if (!find_better_candidate(si, &ev, UEV_SELECT_REASON_ALL, 0))
211 goto out;
212
213 ev.reason = UEV_REASON_BETTER_CANDIDATE;
214 ev.node_cur = si->node;
215 ret = false;
216
217 out:
218 switch (type) {
219 case EVENT_TYPE_PROBE:
220 ev.type = UEV_PROBE_REQ_ACCEPT;
221 break;
222 case EVENT_TYPE_ASSOC:
223 ev.type = UEV_ASSOC_REQ_ACCEPT;
224 break;
225 case EVENT_TYPE_AUTH:
226 ev.type = UEV_AUTH_REQ_ACCEPT;
227 break;
228 default:
229 break;
230 }
231
232 if (!ret)
233 ev.type++;
234
235 if (!ret && si->stats[type].blocked_cur >= config.max_retry_band) {
236 ev.reason = UEV_REASON_RETRY_EXCEEDED;
237 ev.threshold.cur = si->stats[type].blocked_cur;
238 ev.threshold.ref = config.max_retry_band;
239 }
240 usteer_event(&ev);
241
242 return ret;
243 }
244
245 static bool
246 is_more_kickable(struct sta_info *si_cur, struct sta_info *si_new)
247 {
248 if (!si_cur)
249 return true;
250
251 if (si_new->kick_count > si_cur->kick_count)
252 return false;
253
254 return si_cur->signal > si_new->signal;
255 }
256
257 static void
258 usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state,
259 struct uevent *ev)
260 {
261 si->roam_event = current_time;
262
263 if (si->roam_state == state) {
264 if (si->roam_state == ROAM_TRIGGER_IDLE) {
265 si->roam_tries = 0;
266 return;
267 }
268
269 si->roam_tries++;
270 } else {
271 si->roam_tries = 0;
272 }
273
274 si->roam_state = state;
275 usteer_event(ev);
276 }
277
278 static void
279 usteer_roam_sm_start_scan(struct sta_info *si, struct uevent *ev)
280 {
281 /* Start scanning in case we are not timeout-constrained or timeout has expired */
282 if (!config.roam_scan_timeout ||
283 current_time > si->roam_scan_timeout_start + config.roam_scan_timeout) {
284 usteer_roam_set_state(si, ROAM_TRIGGER_SCAN, ev);
285 return;
286 }
287
288 /* We are currently in scan timeout / cooldown.
289 * Check if we are in ROAM_TRIGGER_IDLE state. Enter this state if not.
290 */
291 if (si->roam_state == ROAM_TRIGGER_IDLE)
292 return;
293
294 /* Enter idle state */
295 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
296 }
297
298 static bool
299 usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum roam_trigger_state next_state)
300 {
301 uint64_t max_age = 2 * config.roam_scan_interval;
302
303 if (max_age > current_time - si->roam_scan_start)
304 max_age = current_time - si->roam_scan_start;
305
306 if (find_better_candidate(si, ev, (1 << UEV_SELECT_REASON_SIGNAL), max_age)) {
307 usteer_roam_set_state(si, next_state, ev);
308 return true;
309 }
310
311 return false;
312 }
313
314 static bool
315 usteer_roam_trigger_sm(struct sta_info *si)
316 {
317 struct uevent ev = {
318 .si_cur = si,
319 };
320 uint64_t min_signal;
321
322 min_signal = usteer_snr_to_signal(si->node, config.roam_trigger_snr);
323
324 switch (si->roam_state) {
325 case ROAM_TRIGGER_SCAN:
326 if (!si->roam_tries) {
327 si->roam_scan_start = current_time;
328 }
329
330 /* Check if we've found a better node regardless of the scan-interval */
331 if (usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE))
332 break;
333
334 /* Only scan every scan-interval */
335 if (current_time - si->roam_event < config.roam_scan_interval)
336 break;
337
338 /* Check if no node was found within roam_scan_tries tries */
339 if (config.roam_scan_tries && si->roam_tries >= config.roam_scan_tries) {
340 if (!config.roam_scan_timeout) {
341 /* Prepare to kick client */
342 usteer_roam_set_state(si, ROAM_TRIGGER_WAIT_KICK, &ev);
343 } else {
344 /* Kick in scan timeout */
345 si->roam_scan_timeout_start = current_time;
346 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
347 }
348 break;
349 }
350
351 /* Send beacon-request to client */
352 usteer_ubus_trigger_client_scan(si);
353 usteer_roam_sm_start_scan(si, &ev);
354 break;
355
356 case ROAM_TRIGGER_IDLE:
357 usteer_roam_sm_start_scan(si, &ev);
358 break;
359
360 case ROAM_TRIGGER_SCAN_DONE:
361 if (usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_WAIT_KICK))
362 break;
363
364 /* Kick back to SCAN state if candidate expired */
365 usteer_roam_sm_start_scan(si, &ev);
366 break;
367
368 case ROAM_TRIGGER_WAIT_KICK:
369 if (si->signal > min_signal)
370 break;
371
372 usteer_roam_set_state(si, ROAM_TRIGGER_NOTIFY_KICK, &ev);
373 usteer_ubus_notify_client_disassoc(si);
374 break;
375 case ROAM_TRIGGER_NOTIFY_KICK:
376 if (current_time - si->roam_event < config.roam_kick_delay * 100)
377 break;
378
379 usteer_roam_set_state(si, ROAM_TRIGGER_KICK, &ev);
380 break;
381 case ROAM_TRIGGER_KICK:
382 usteer_ubus_kick_client(si);
383 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
384 return true;
385 }
386
387 return false;
388 }
389
390 static void
391 usteer_local_node_roam_check(struct usteer_local_node *ln, struct uevent *ev)
392 {
393 struct sta_info *si;
394 int min_signal;
395
396 if (config.roam_scan_snr)
397 min_signal = config.roam_scan_snr;
398 else if (config.roam_trigger_snr)
399 min_signal = config.roam_trigger_snr;
400 else
401 return;
402
403 usteer_update_time();
404 min_signal = usteer_snr_to_signal(&ln->node, min_signal);
405
406 list_for_each_entry(si, &ln->node.sta_info, node_list) {
407 if (si->connected != STA_CONNECTED || si->signal >= min_signal ||
408 current_time - si->roam_kick < config.roam_trigger_interval) {
409 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
410 continue;
411 }
412
413 /*
414 * If the state machine kicked a client, other clients should wait
415 * until the next turn
416 */
417 if (usteer_roam_trigger_sm(si))
418 return;
419 }
420 }
421
422 static void
423 usteer_local_node_snr_kick(struct usteer_local_node *ln)
424 {
425 struct uevent ev = {
426 .node_local = &ln->node,
427 };
428 struct sta_info *si;
429 int min_signal;
430
431 if (!config.min_snr)
432 return;
433
434 min_signal = usteer_snr_to_signal(&ln->node, config.min_snr);
435 ev.threshold.ref = min_signal;
436
437 list_for_each_entry(si, &ln->node.sta_info, node_list) {
438 if (si->connected != STA_CONNECTED)
439 continue;
440
441 if (si->signal >= min_signal)
442 continue;
443
444 si->kick_count++;
445
446 ev.type = UEV_SIGNAL_KICK;
447 ev.threshold.cur = si->signal;
448 ev.count = si->kick_count;
449 usteer_event(&ev);
450
451 usteer_ubus_kick_client(si);
452 return;
453 }
454 }
455
456 void
457 usteer_local_node_kick(struct usteer_local_node *ln)
458 {
459 struct usteer_node *node = &ln->node;
460 struct sta_info *kick1 = NULL, *kick2 = NULL;
461 struct sta_info *candidate = NULL;
462 struct sta_info *si;
463 struct uevent ev = {
464 .node_local = &ln->node,
465 };
466 unsigned int min_count = DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update);
467
468 usteer_local_node_roam_check(ln, &ev);
469 usteer_local_node_snr_kick(ln);
470
471 if (!config.load_kick_enabled || !config.load_kick_threshold ||
472 !config.load_kick_delay)
473 return;
474
475 if (node->load < config.load_kick_threshold) {
476 if (!ln->load_thr_count)
477 return;
478
479 ln->load_thr_count = 0;
480 ev.type = UEV_LOAD_KICK_RESET;
481 ev.threshold.cur = node->load;
482 ev.threshold.ref = config.load_kick_threshold;
483 goto out;
484 }
485
486 if (++ln->load_thr_count <= min_count) {
487 if (ln->load_thr_count > 1)
488 return;
489
490 ev.type = UEV_LOAD_KICK_TRIGGER;
491 ev.threshold.cur = node->load;
492 ev.threshold.ref = config.load_kick_threshold;
493 goto out;
494 }
495
496 ln->load_thr_count = 0;
497 if (node->n_assoc < config.load_kick_min_clients) {
498 ev.type = UEV_LOAD_KICK_MIN_CLIENTS;
499 ev.threshold.cur = node->n_assoc;
500 ev.threshold.ref = config.load_kick_min_clients;
501 goto out;
502 }
503
504 list_for_each_entry(si, &ln->node.sta_info, node_list) {
505 struct sta_info *tmp;
506
507 if (si->connected != STA_CONNECTED)
508 continue;
509
510 if (is_more_kickable(kick1, si))
511 kick1 = si;
512
513 tmp = find_better_candidate(si, NULL, (1 << UEV_SELECT_REASON_LOAD), 0);
514 if (!tmp)
515 continue;
516
517 if (is_more_kickable(kick2, si)) {
518 kick2 = si;
519 candidate = tmp;
520 }
521 }
522
523 if (!kick1) {
524 ev.type = UEV_LOAD_KICK_NO_CLIENT;
525 goto out;
526 }
527
528 if (kick2)
529 kick1 = kick2;
530
531 kick1->kick_count++;
532
533 ev.type = UEV_LOAD_KICK_CLIENT;
534 ev.si_cur = kick1;
535 ev.si_other = candidate;
536 ev.count = kick1->kick_count;
537
538 usteer_ubus_kick_client(kick1);
539
540 out:
541 usteer_event(&ev);
542 }