policy: fix incurrect handling of scan-requests with disabled timeout
[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)
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 reasons = is_better_candidate(si_ref, si);
130 if (!reasons)
131 continue;
132
133 if (!(reasons & required_criteria))
134 continue;
135
136 if (ev) {
137 ev->si_other = si;
138 ev->select_reasons = reasons;
139 }
140
141 return si;
142 }
143
144 return NULL;
145 }
146
147 int
148 usteer_snr_to_signal(struct usteer_node *node, int snr)
149 {
150 int noise = -95;
151
152 if (snr < 0)
153 return snr;
154
155 if (node->noise)
156 noise = node->noise;
157
158 return noise + snr;
159 }
160
161 bool
162 usteer_check_request(struct sta_info *si, enum usteer_event_type type)
163 {
164 struct uevent ev = {
165 .si_cur = si,
166 };
167 int min_signal;
168 bool ret = true;
169
170 if (type == EVENT_TYPE_AUTH)
171 goto out;
172
173 if (type == EVENT_TYPE_ASSOC) {
174 /* Check if assoc request has lower signal than min_signal.
175 * If this is the case, block assoc even when assoc steering is enabled.
176 *
177 * Otherwise, the client potentially ends up in a assoc - kick loop.
178 */
179 if (config.min_snr && si->signal < usteer_snr_to_signal(si->node, config.min_snr)) {
180 ev.reason = UEV_REASON_LOW_SIGNAL;
181 ev.threshold.cur = si->signal;
182 ev.threshold.ref = usteer_snr_to_signal(si->node, config.min_snr);
183 ret = false;
184 goto out;
185 } else if (!config.assoc_steering) {
186 goto out;
187 }
188 }
189
190 min_signal = usteer_snr_to_signal(si->node, config.min_connect_snr);
191 if (si->signal < min_signal) {
192 ev.reason = UEV_REASON_LOW_SIGNAL;
193 ev.threshold.cur = si->signal;
194 ev.threshold.ref = min_signal;
195 ret = false;
196 goto out;
197 }
198
199 if (current_time - si->created < config.initial_connect_delay) {
200 ev.reason = UEV_REASON_CONNECT_DELAY;
201 ev.threshold.cur = current_time - si->created;
202 ev.threshold.ref = config.initial_connect_delay;
203 ret = false;
204 goto out;
205 }
206
207 if (!find_better_candidate(si, &ev, UEV_SELECT_REASON_ALL))
208 goto out;
209
210 ev.reason = UEV_REASON_BETTER_CANDIDATE;
211 ev.node_cur = si->node;
212 ret = false;
213
214 out:
215 switch (type) {
216 case EVENT_TYPE_PROBE:
217 ev.type = UEV_PROBE_REQ_ACCEPT;
218 break;
219 case EVENT_TYPE_ASSOC:
220 ev.type = UEV_ASSOC_REQ_ACCEPT;
221 break;
222 case EVENT_TYPE_AUTH:
223 ev.type = UEV_AUTH_REQ_ACCEPT;
224 break;
225 default:
226 break;
227 }
228
229 if (!ret)
230 ev.type++;
231
232 if (!ret && si->stats[type].blocked_cur >= config.max_retry_band) {
233 ev.reason = UEV_REASON_RETRY_EXCEEDED;
234 ev.threshold.cur = si->stats[type].blocked_cur;
235 ev.threshold.ref = config.max_retry_band;
236 }
237 usteer_event(&ev);
238
239 return ret;
240 }
241
242 static bool
243 is_more_kickable(struct sta_info *si_cur, struct sta_info *si_new)
244 {
245 if (!si_cur)
246 return true;
247
248 if (si_new->kick_count > si_cur->kick_count)
249 return false;
250
251 return si_cur->signal > si_new->signal;
252 }
253
254 static void
255 usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state,
256 struct uevent *ev)
257 {
258 si->roam_event = current_time;
259
260 if (si->roam_state == state) {
261 if (si->roam_state == ROAM_TRIGGER_IDLE) {
262 si->roam_tries = 0;
263 return;
264 }
265
266 si->roam_tries++;
267 } else {
268 si->roam_tries = 0;
269 }
270
271 si->roam_state = state;
272 usteer_event(ev);
273 }
274
275 static void
276 usteer_roam_sm_start_scan(struct sta_info *si, struct uevent *ev)
277 {
278 /* Start scanning in case we are not timeout-constrained or timeout has expired */
279 if (!config.roam_scan_timeout ||
280 current_time > si->roam_scan_timeout_start + config.roam_scan_timeout) {
281 usteer_roam_set_state(si, ROAM_TRIGGER_SCAN, ev);
282 return;
283 }
284
285 /* We are currently in scan timeout / cooldown.
286 * Check if we are in ROAM_TRIGGER_IDLE state and enter this stateif not.
287 */
288 if (si->roam_state == ROAM_TRIGGER_IDLE)
289 return;
290
291 /* Enter idle state */
292 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
293 }
294
295 static bool
296 usteer_roam_trigger_sm(struct sta_info *si)
297 {
298 struct uevent ev = {
299 .si_cur = si,
300 };
301 int min_signal;
302
303 min_signal = usteer_snr_to_signal(si->node, config.roam_trigger_snr);
304
305 switch (si->roam_state) {
306 case ROAM_TRIGGER_SCAN:
307 if (current_time - si->roam_event < config.roam_scan_interval)
308 break;
309
310 if (find_better_candidate(si, &ev, (1 << UEV_SELECT_REASON_SIGNAL)) ||
311 si->roam_scan_done > si->roam_event) {
312 usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE, &ev);
313 break;
314 }
315
316 if (config.roam_scan_tries && si->roam_tries >= config.roam_scan_tries) {
317 if (!config.roam_scan_timeout) {
318 /* Prepare to kick client */
319 usteer_roam_set_state(si, ROAM_TRIGGER_WAIT_KICK, &ev);
320 } else {
321 /* Kick in scan timeout */
322 si->roam_scan_timeout_start = current_time;
323 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
324 }
325 break;
326 }
327
328 usteer_ubus_trigger_client_scan(si);
329 usteer_roam_sm_start_scan(si, &ev);
330 break;
331
332 case ROAM_TRIGGER_IDLE:
333 if (find_better_candidate(si, &ev, (1 << UEV_SELECT_REASON_SIGNAL))) {
334 usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE, &ev);
335 break;
336 }
337
338 usteer_roam_sm_start_scan(si, &ev);
339 break;
340
341 case ROAM_TRIGGER_SCAN_DONE:
342 /* Check for stale scan results, kick back to SCAN state if necessary */
343 if (current_time - si->roam_scan_done > 2 * config.roam_scan_interval) {
344 usteer_roam_sm_start_scan(si, &ev);
345 break;
346 }
347
348 if (find_better_candidate(si, &ev, (1 << UEV_SELECT_REASON_SIGNAL)))
349 usteer_roam_set_state(si, ROAM_TRIGGER_WAIT_KICK, &ev);
350
351 break;
352
353 case ROAM_TRIGGER_WAIT_KICK:
354 if (si->signal > min_signal)
355 break;
356
357 usteer_roam_set_state(si, ROAM_TRIGGER_NOTIFY_KICK, &ev);
358 usteer_ubus_notify_client_disassoc(si);
359 break;
360 case ROAM_TRIGGER_NOTIFY_KICK:
361 if (current_time - si->roam_event < config.roam_kick_delay * 100)
362 break;
363
364 usteer_roam_set_state(si, ROAM_TRIGGER_KICK, &ev);
365 break;
366 case ROAM_TRIGGER_KICK:
367 usteer_ubus_kick_client(si);
368 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
369 return true;
370 }
371
372 return false;
373 }
374
375 static void
376 usteer_local_node_roam_check(struct usteer_local_node *ln, struct uevent *ev)
377 {
378 struct sta_info *si;
379 int min_signal;
380
381 if (config.roam_scan_snr)
382 min_signal = config.roam_scan_snr;
383 else if (config.roam_trigger_snr)
384 min_signal = config.roam_trigger_snr;
385 else
386 return;
387
388 usteer_update_time();
389 min_signal = usteer_snr_to_signal(&ln->node, min_signal);
390
391 list_for_each_entry(si, &ln->node.sta_info, node_list) {
392 if (si->connected != STA_CONNECTED || si->signal >= min_signal ||
393 current_time - si->roam_kick < config.roam_trigger_interval) {
394 usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
395 continue;
396 }
397
398 /*
399 * If the state machine kicked a client, other clients should wait
400 * until the next turn
401 */
402 if (usteer_roam_trigger_sm(si))
403 return;
404 }
405 }
406
407 static void
408 usteer_local_node_snr_kick(struct usteer_local_node *ln)
409 {
410 struct uevent ev = {
411 .node_local = &ln->node,
412 };
413 struct sta_info *si;
414 int min_signal;
415
416 if (!config.min_snr)
417 return;
418
419 min_signal = usteer_snr_to_signal(&ln->node, config.min_snr);
420 ev.threshold.ref = min_signal;
421
422 list_for_each_entry(si, &ln->node.sta_info, node_list) {
423 if (si->connected != STA_CONNECTED)
424 continue;
425
426 if (si->signal >= min_signal)
427 continue;
428
429 si->kick_count++;
430
431 ev.type = UEV_SIGNAL_KICK;
432 ev.threshold.cur = si->signal;
433 ev.count = si->kick_count;
434 usteer_event(&ev);
435
436 usteer_ubus_kick_client(si);
437 return;
438 }
439 }
440
441 void
442 usteer_local_node_kick(struct usteer_local_node *ln)
443 {
444 struct usteer_node *node = &ln->node;
445 struct sta_info *kick1 = NULL, *kick2 = NULL;
446 struct sta_info *candidate = NULL;
447 struct sta_info *si;
448 struct uevent ev = {
449 .node_local = &ln->node,
450 };
451 unsigned int min_count = DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update);
452
453 usteer_local_node_roam_check(ln, &ev);
454 usteer_local_node_snr_kick(ln);
455
456 if (!config.load_kick_enabled || !config.load_kick_threshold ||
457 !config.load_kick_delay)
458 return;
459
460 if (node->load < config.load_kick_threshold) {
461 if (!ln->load_thr_count)
462 return;
463
464 ln->load_thr_count = 0;
465 ev.type = UEV_LOAD_KICK_RESET;
466 ev.threshold.cur = node->load;
467 ev.threshold.ref = config.load_kick_threshold;
468 goto out;
469 }
470
471 if (++ln->load_thr_count <= min_count) {
472 if (ln->load_thr_count > 1)
473 return;
474
475 ev.type = UEV_LOAD_KICK_TRIGGER;
476 ev.threshold.cur = node->load;
477 ev.threshold.ref = config.load_kick_threshold;
478 goto out;
479 }
480
481 ln->load_thr_count = 0;
482 if (node->n_assoc < config.load_kick_min_clients) {
483 ev.type = UEV_LOAD_KICK_MIN_CLIENTS;
484 ev.threshold.cur = node->n_assoc;
485 ev.threshold.ref = config.load_kick_min_clients;
486 goto out;
487 }
488
489 list_for_each_entry(si, &ln->node.sta_info, node_list) {
490 struct sta_info *tmp;
491
492 if (si->connected != STA_CONNECTED)
493 continue;
494
495 if (is_more_kickable(kick1, si))
496 kick1 = si;
497
498 tmp = find_better_candidate(si, NULL, (1 << UEV_SELECT_REASON_LOAD));
499 if (!tmp)
500 continue;
501
502 if (is_more_kickable(kick2, si)) {
503 kick2 = si;
504 candidate = tmp;
505 }
506 }
507
508 if (!kick1) {
509 ev.type = UEV_LOAD_KICK_NO_CLIENT;
510 goto out;
511 }
512
513 if (kick2)
514 kick1 = kick2;
515
516 kick1->kick_count++;
517
518 ev.type = UEV_LOAD_KICK_CLIENT;
519 ev.si_cur = kick1;
520 ev.si_other = candidate;
521 ev.count = kick1->kick_count;
522
523 usteer_ubus_kick_client(kick1);
524
525 out:
526 usteer_event(&ev);
527 }