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