map: improve timeout handling of IP entries
[project/qosify.git] / map.c
1 // SPDX-License-Identifier: GPL-2.0+
2 /*
3 * Copyright (C) 2021 Felix Fietkau <nbd@nbd.name>
4 */
5 #include <arpa/inet.h>
6
7 #include <errno.h>
8 #include <stdio.h>
9 #include <ctype.h>
10 #include <stdlib.h>
11 #include <time.h>
12
13 #include <libubox/uloop.h>
14
15 #include "qosify.h"
16
17 static int qosify_map_entry_cmp(const void *k1, const void *k2, void *ptr);
18
19 static int qosify_map_fds[__CL_MAP_MAX];
20 static AVL_TREE(map_data, qosify_map_entry_cmp, false, NULL);
21 static LIST_HEAD(map_files);
22 static uint32_t next_timeout;
23 static uint8_t qosify_dscp_default[2] = { 0xff, 0xff };
24 int qosify_map_timeout;
25 int qosify_active_timeout;
26 struct qosify_config config;
27
28 struct qosify_map_file {
29 struct list_head list;
30 char filename[];
31 };
32
33 static const struct {
34 const char *name;
35 const char *type_name;
36 } qosify_map_info[] = {
37 [CL_MAP_TCP_PORTS] = { "tcp_ports", "tcp_port" },
38 [CL_MAP_UDP_PORTS] = { "udp_ports", "udp_port" },
39 [CL_MAP_IPV4_ADDR] = { "ipv4_map", "ipv4_addr" },
40 [CL_MAP_IPV6_ADDR] = { "ipv6_map", "ipv6_addr" },
41 [CL_MAP_CONFIG] = { "config", "config" },
42 [CL_MAP_DNS] = { "dns", "dns" },
43 };
44
45 static const struct {
46 const char name[5];
47 uint8_t val;
48 } codepoints[] = {
49 { "CS0", 0 },
50 { "CS1", 8 },
51 { "CS2", 16 },
52 { "CS3", 24 },
53 { "CS4", 32 },
54 { "CS5", 40 },
55 { "CS6", 48 },
56 { "CS7", 56 },
57 { "AF11", 10 },
58 { "AF12", 12 },
59 { "AF13", 14 },
60 { "AF21", 18 },
61 { "AF22", 20 },
62 { "AF22", 22 },
63 { "AF31", 26 },
64 { "AF32", 28 },
65 { "AF33", 30 },
66 { "AF41", 34 },
67 { "AF42", 36 },
68 { "AF43", 38 },
69 { "EF", 46 },
70 { "VA", 44 },
71 { "LE", 1 },
72 };
73
74 static void qosify_map_timer_cb(struct uloop_timeout *t)
75 {
76 qosify_map_gc();
77 }
78
79 static struct uloop_timeout qosify_map_timer = {
80 .cb = qosify_map_timer_cb,
81 };
82
83 static uint32_t qosify_gettime(void)
84 {
85 struct timespec ts;
86
87 clock_gettime(CLOCK_MONOTONIC, &ts);
88
89 return ts.tv_sec;
90 }
91
92 static const char *
93 qosify_map_path(enum qosify_map_id id)
94 {
95 static char path[128];
96 const char *name;
97
98 if (id >= ARRAY_SIZE(qosify_map_info))
99 return NULL;
100
101 name = qosify_map_info[id].name;
102 if (!name)
103 return NULL;
104
105 snprintf(path, sizeof(path), "%s/%s", CLASSIFY_DATA_PATH, name);
106
107 return path;
108 }
109
110 static int qosify_map_get_fd(enum qosify_map_id id)
111 {
112 const char *path = qosify_map_path(id);
113 int fd;
114
115 if (!path)
116 return -1;
117
118 fd = bpf_obj_get(path);
119 if (fd < 0)
120 fprintf(stderr, "Failed to open map %s: %s\n", path, strerror(errno));
121
122 return fd;
123 }
124
125 static void qosify_map_clear_list(enum qosify_map_id id)
126 {
127 int fd = qosify_map_fds[id];
128 __u32 key[4] = {};
129
130 while (bpf_map_get_next_key(fd, &key, &key) != -1)
131 bpf_map_delete_elem(fd, &key);
132 }
133
134 static void __qosify_map_set_dscp_default(enum qosify_map_id id, uint8_t val)
135 {
136 struct qosify_map_data data = {
137 .id = id,
138 };
139 int fd = qosify_map_fds[id];
140 int i;
141
142 val |= QOSIFY_DSCP_DEFAULT_FLAG;
143
144 for (i = 0; i < (1 << 16); i++) {
145 data.addr.port = htons(i);
146 if (avl_find(&map_data, &data))
147 continue;
148
149 bpf_map_update_elem(fd, &data.addr, &val, BPF_ANY);
150 }
151 }
152
153 void qosify_map_set_dscp_default(enum qosify_map_id id, uint8_t val)
154 {
155 bool udp;
156
157 if (id == CL_MAP_TCP_PORTS)
158 udp = false;
159 else if (id == CL_MAP_UDP_PORTS)
160 udp = true;
161 else
162 return;
163
164 if (qosify_dscp_default[udp] == val)
165 return;
166
167 qosify_dscp_default[udp] = val;
168 __qosify_map_set_dscp_default(id, val);
169 }
170
171 int qosify_map_init(void)
172 {
173 int i;
174
175 for (i = 0; i < CL_MAP_DNS; i++) {
176 qosify_map_fds[i] = qosify_map_get_fd(i);
177 if (qosify_map_fds[i] < 0)
178 return -1;
179 }
180
181 qosify_map_clear_list(CL_MAP_IPV4_ADDR);
182 qosify_map_clear_list(CL_MAP_IPV6_ADDR);
183 qosify_map_reset_config();
184
185 return 0;
186 }
187
188 static char *str_skip(char *str, bool space)
189 {
190 while (*str && isspace(*str) == space)
191 str++;
192
193 return str;
194 }
195
196 static int
197 qosify_map_codepoint(const char *val)
198 {
199 int i;
200
201 for (i = 0; i < ARRAY_SIZE(codepoints); i++)
202 if (!strcmp(codepoints[i].name, val))
203 return codepoints[i].val;
204
205 return 0xff;
206 }
207
208 static int qosify_map_entry_cmp(const void *k1, const void *k2, void *ptr)
209 {
210 const struct qosify_map_data *d1 = k1;
211 const struct qosify_map_data *d2 = k2;
212
213 if (d1->id != d2->id)
214 return d2->id - d1->id;
215
216 if (d1->id == CL_MAP_DNS)
217 return strcmp(d1->addr.dns.pattern, d2->addr.dns.pattern);
218
219 return memcmp(&d1->addr, &d2->addr, sizeof(d1->addr));
220 }
221
222 static struct qosify_map_entry *
223 __qosify_map_alloc_entry(struct qosify_map_data *data)
224 {
225 struct qosify_map_entry *e;
226 char *pattern;
227
228 if (data->id < CL_MAP_DNS) {
229 e = calloc(1, sizeof(*e));
230 memcpy(&e->data.addr, &data->addr, sizeof(e->data.addr));
231
232 return e;
233 }
234
235 e = calloc_a(sizeof(*e), &pattern, strlen(data->addr.dns.pattern) + 1);
236 strcpy(pattern, data->addr.dns.pattern);
237 e->data.addr.dns.pattern = pattern;
238 if (regcomp(&e->data.addr.dns.regex, pattern,
239 REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
240 free(e);
241 return NULL;
242 }
243
244 return e;
245 }
246
247 static void __qosify_map_set_entry(struct qosify_map_data *data)
248 {
249 int fd = qosify_map_fds[data->id];
250 struct qosify_map_entry *e;
251 bool file = data->file;
252 int32_t delta = 0;
253 bool add = data->dscp != 0xff;
254 uint8_t prev_dscp = 0xff;
255
256 e = avl_find_element(&map_data, data, e, avl);
257 if (!e) {
258 if (!add)
259 return;
260
261 e = __qosify_map_alloc_entry(data);
262 if (!e)
263 return;
264
265 e->avl.key = &e->data;
266 e->data.id = data->id;
267 avl_insert(&map_data, &e->avl);
268 } else {
269 prev_dscp = e->data.dscp;
270 }
271
272 if (file)
273 e->data.file = add;
274 else
275 e->data.user = add;
276
277 if (add) {
278 if (file)
279 e->data.file_dscp = data->dscp;
280 if (!e->data.user || !file)
281 e->data.dscp = data->dscp;
282 } else if (e->data.file && !file) {
283 e->data.dscp = e->data.file_dscp;
284 }
285
286 if (e->data.dscp != prev_dscp && data->id < CL_MAP_DNS) {
287 struct qosify_ip_map_val val = {
288 .dscp = e->data.dscp,
289 .seen = 1,
290 };
291
292 bpf_map_update_elem(fd, &data->addr, &val, BPF_ANY);
293 }
294
295 if (add) {
296 if (qosify_map_timeout == ~0 || file) {
297 e->timeout = ~0;
298 return;
299 }
300
301 e->timeout = qosify_gettime() + qosify_map_timeout;
302 delta = e->timeout - next_timeout;
303 if (next_timeout && delta >= 0)
304 return;
305 }
306
307 uloop_timeout_set(&qosify_map_timer, 1);
308 }
309
310 static int
311 qosify_map_set_port(struct qosify_map_data *data, const char *str)
312 {
313 unsigned long start_port, end_port;
314 char *err;
315 int i;
316
317 start_port = end_port = strtoul(str, &err, 0);
318 if (err && *err) {
319 if (*err == '-')
320 end_port = strtoul(err + 1, &err, 0);
321 if (*err)
322 return -1;
323 }
324
325 if (!start_port || end_port < start_port ||
326 end_port >= 65535)
327 return -1;
328
329 for (i = start_port; i <= end_port; i++) {
330 data->addr.port = htons(i);
331 __qosify_map_set_entry(data);
332 }
333
334 return 0;
335 }
336
337 static int
338 qosify_map_fill_ip(struct qosify_map_data *data, const char *str)
339 {
340 int af;
341
342 if (data->id == CL_MAP_IPV6_ADDR)
343 af = AF_INET6;
344 else
345 af = AF_INET;
346
347 if (inet_pton(af, str, &data->addr) != 1)
348 return -1;
349
350 return 0;
351 }
352
353 int qosify_map_set_entry(enum qosify_map_id id, bool file, const char *str, uint8_t dscp)
354 {
355 struct qosify_map_data data = {
356 .id = id,
357 .file = file,
358 .dscp = dscp,
359 };
360
361 switch (id) {
362 case CL_MAP_DNS:
363 data.addr.dns.pattern = str;
364 break;
365 case CL_MAP_TCP_PORTS:
366 case CL_MAP_UDP_PORTS:
367 return qosify_map_set_port(&data, str);
368 case CL_MAP_IPV4_ADDR:
369 case CL_MAP_IPV6_ADDR:
370 if (qosify_map_fill_ip(&data, str))
371 return -1;
372 break;
373 default:
374 return -1;
375 }
376
377 __qosify_map_set_entry(&data);
378
379 return 0;
380 }
381
382 int qosify_map_dscp_value(const char *val)
383 {
384 unsigned long dscp;
385 char *err;
386 bool fallback = false;
387
388 if (*val == '+') {
389 fallback = true;
390 val++;
391 }
392
393 dscp = strtoul(val, &err, 0);
394 if (err && *err)
395 dscp = qosify_map_codepoint(val);
396
397 if (dscp >= 64)
398 return -1;
399
400 return dscp + (fallback << 6);
401 }
402
403 static void
404 qosify_map_dscp_codepoint_str(char *dest, int len, uint8_t dscp)
405 {
406 int i;
407
408 if (dscp & QOSIFY_DSCP_FALLBACK_FLAG) {
409 *(dest++) = '+';
410 len--;
411 dscp &= ~QOSIFY_DSCP_FALLBACK_FLAG;
412 }
413
414 for (i = 0; i < ARRAY_SIZE(codepoints); i++) {
415 if (codepoints[i].val != dscp)
416 continue;
417
418 snprintf(dest, len, "%s", codepoints[i].name);
419 return;
420 }
421
422 snprintf(dest, len, "0x%x", dscp);
423 }
424
425 static void
426 qosify_map_parse_line(char *str)
427 {
428 const char *key, *value;
429 int dscp;
430
431 str = str_skip(str, true);
432 key = str;
433
434 str = str_skip(str, false);
435 if (!*str)
436 return;
437
438 *(str++) = 0;
439 str = str_skip(str, true);
440 value = str;
441
442 dscp = qosify_map_dscp_value(value);
443 if (dscp < 0)
444 return;
445
446 if (!strncmp(key, "dns:", 4))
447 qosify_map_set_entry(CL_MAP_DNS, true, key + 4, dscp);
448 if (!strncmp(key, "tcp:", 4))
449 qosify_map_set_entry(CL_MAP_TCP_PORTS, true, key + 4, dscp);
450 else if (!strncmp(key, "udp:", 4))
451 qosify_map_set_entry(CL_MAP_UDP_PORTS, true, key + 4, dscp);
452 else if (strchr(key, ':'))
453 qosify_map_set_entry(CL_MAP_IPV6_ADDR, true, key, dscp);
454 else if (strchr(key, '.'))
455 qosify_map_set_entry(CL_MAP_IPV4_ADDR, true, key, dscp);
456 }
457
458 static int __qosify_map_load_file(const char *file)
459 {
460 char line[1024];
461 char *cur;
462 FILE *f;
463
464 if (!file)
465 return 0;
466
467 f = fopen(file, "r");
468 if (!f) {
469 fprintf(stderr, "Can't open data file %s\n", file);
470 return -1;
471 }
472
473 while (fgets(line, sizeof(line), f)) {
474 cur = strchr(line, '#');
475 if (cur)
476 *cur = 0;
477
478 cur = line + strlen(line);
479 if (cur == line)
480 continue;
481
482 while (cur > line && isspace(cur[-1]))
483 cur--;
484
485 *cur = 0;
486 qosify_map_parse_line(line);
487 }
488
489 fclose(f);
490
491 return 0;
492 }
493
494 int qosify_map_load_file(const char *file)
495 {
496 struct qosify_map_file *f;
497
498 if (!file)
499 return 0;
500
501 f = calloc(1, sizeof(*f) + strlen(file) + 1);
502 strcpy(f->filename, file);
503 list_add_tail(&f->list, &map_files);
504
505 return __qosify_map_load_file(file);
506 }
507
508 static void qosify_map_reset_file_entries(void)
509 {
510 struct qosify_map_entry *e;
511
512 avl_for_each_element(&map_data, e, avl)
513 e->data.file = false;
514 }
515
516 void qosify_map_clear_files(void)
517 {
518 struct qosify_map_file *f, *tmp;
519
520 qosify_map_reset_file_entries();
521
522 list_for_each_entry_safe(f, tmp, &map_files, list) {
523 list_del(&f->list);
524 free(f);
525 }
526 }
527
528 void qosify_map_reset_config(void)
529 {
530 qosify_map_clear_files();
531 qosify_map_set_dscp_default(CL_MAP_TCP_PORTS, 0);
532 qosify_map_set_dscp_default(CL_MAP_UDP_PORTS, 0);
533 qosify_map_timeout = 3600;
534 qosify_active_timeout = 60;
535
536 memset(&config, 0, sizeof(config));
537 config.dscp_prio = 0xff;
538 config.dscp_bulk = 0xff;
539 config.dscp_icmp = 0xff;
540 }
541
542 void qosify_map_reload(void)
543 {
544 struct qosify_map_file *f;
545
546 qosify_map_reset_file_entries();
547
548 list_for_each_entry(f, &map_files, list)
549 __qosify_map_load_file(f->filename);
550
551 qosify_map_gc();
552 }
553
554 static void qosify_map_free_entry(struct qosify_map_entry *e)
555 {
556 int fd = qosify_map_fds[e->data.id];
557
558 avl_delete(&map_data, &e->avl);
559 if (e->data.id < CL_MAP_DNS)
560 bpf_map_delete_elem(fd, &e->data.addr);
561 free(e);
562 }
563
564 static bool
565 qosify_map_entry_refresh_timeout(struct qosify_map_entry *e)
566 {
567 struct qosify_ip_map_val val;
568 int fd = qosify_map_fds[e->data.id];
569
570 if (e->data.id != CL_MAP_IPV4_ADDR &&
571 e->data.id != CL_MAP_IPV6_ADDR)
572 return false;
573
574 if (bpf_map_lookup_elem(fd, &e->data.addr, &val))
575 return false;
576
577 if (!val.seen)
578 return false;
579
580 e->timeout = qosify_gettime() + qosify_active_timeout;
581 val.seen = 0;
582 bpf_map_update_elem(fd, &e->data.addr, &val, BPF_ANY);
583
584 return true;
585 }
586
587 void qosify_map_gc(void)
588 {
589 struct qosify_map_entry *e, *tmp;
590 int32_t timeout = 0;
591 uint32_t cur_time = qosify_gettime();
592
593 next_timeout = 0;
594 avl_for_each_element_safe(&map_data, e, avl, tmp) {
595 int32_t cur_timeout;
596
597 if (e->data.user && e->timeout != ~0) {
598 cur_timeout = e->timeout - cur_time;
599 if (cur_timeout <= 0 &&
600 qosify_map_entry_refresh_timeout(e))
601 cur_timeout = e->timeout - cur_time;
602 if (cur_timeout <= 0) {
603 e->data.user = false;
604 e->data.dscp = e->data.file_dscp;
605 } else if (!timeout || cur_timeout < timeout) {
606 timeout = cur_timeout;
607 next_timeout = e->timeout;
608 }
609 }
610
611 if (e->data.file || e->data.user)
612 continue;
613
614 qosify_map_free_entry(e);
615 }
616
617 if (!timeout)
618 return;
619
620 uloop_timeout_set(&qosify_map_timer, timeout * 1000);
621 }
622
623
624 int qosify_map_add_dns_host(const char *host, const char *addr, const char *type, int ttl)
625 {
626 struct qosify_map_data data = {
627 .id = CL_MAP_DNS,
628 .addr.dns.pattern = "",
629 };
630 struct qosify_map_entry *e;
631 int prev_timeout = qosify_map_timeout;
632
633 e = avl_find_ge_element(&map_data, &data, e, avl);
634 if (!e)
635 return 0;
636
637 memset(&data, 0, sizeof(data));
638 data.user = true;
639 if (!strcmp(type, "A"))
640 data.id = CL_MAP_IPV4_ADDR;
641 else if (!strcmp(type, "AAAA"))
642 data.id = CL_MAP_IPV6_ADDR;
643 else
644 return 0;
645
646 if (qosify_map_fill_ip(&data, addr))
647 return -1;
648
649 avl_for_element_to_last(&map_data, e, e, avl) {
650 regex_t *regex = &e->data.addr.dns.regex;
651
652 if (e->data.id != CL_MAP_DNS)
653 return 0;
654
655 if (regexec(regex, host, 0, NULL, 0) != 0)
656 continue;
657
658 if (ttl)
659 qosify_map_timeout = ttl;
660 data.dscp = e->data.dscp;
661 __qosify_map_set_entry(&data);
662 qosify_map_timeout = prev_timeout;
663 }
664
665 return 0;
666 }
667
668
669 void qosify_map_dump(struct blob_buf *b)
670 {
671 struct qosify_map_entry *e;
672 uint32_t cur_time = qosify_gettime();
673 int buf_len = INET6_ADDRSTRLEN + 1;
674 char *buf;
675 void *a;
676 int af;
677
678 a = blobmsg_open_array(b, "entries");
679 avl_for_each_element(&map_data, e, avl) {
680 void *c;
681
682 if (!e->data.file && !e->data.user)
683 continue;
684
685 c = blobmsg_open_table(b, NULL);
686 if (e->data.user && e->timeout != ~0) {
687 int32_t cur_timeout = e->timeout - cur_time;
688
689 if (cur_timeout < 0)
690 cur_timeout = 0;
691
692 blobmsg_add_u32(b, "timeout", cur_timeout);
693 }
694
695 blobmsg_add_u8(b, "file", e->data.file);
696 blobmsg_add_u8(b, "user", e->data.user);
697
698 buf = blobmsg_alloc_string_buffer(b, "dscp", buf_len);
699 qosify_map_dscp_codepoint_str(buf, buf_len, e->data.dscp);
700 blobmsg_add_string_buffer(b);
701
702 blobmsg_add_string(b, "type", qosify_map_info[e->data.id].type_name);
703
704 switch (e->data.id) {
705 case CL_MAP_TCP_PORTS:
706 case CL_MAP_UDP_PORTS:
707 blobmsg_printf(b, "addr", "%d", ntohs(e->data.addr.port));
708 break;
709 case CL_MAP_IPV4_ADDR:
710 case CL_MAP_IPV6_ADDR:
711 buf = blobmsg_alloc_string_buffer(b, "addr", buf_len);
712 af = e->data.id == CL_MAP_IPV6_ADDR ? AF_INET6 : AF_INET;
713 inet_ntop(af, &e->data.addr, buf, buf_len);
714 blobmsg_add_string_buffer(b);
715 break;
716 case CL_MAP_DNS:
717 blobmsg_add_string(b, "addr", e->data.addr.dns.pattern);
718 break;
719 default:
720 *buf = 0;
721 break;
722 }
723 blobmsg_close_table(b, c);
724 }
725 blobmsg_close_array(b, a);
726 }
727
728 void qosify_map_update_config(void)
729 {
730 int fd = qosify_map_fds[CL_MAP_CONFIG];
731 uint32_t key = 0;
732
733 bpf_map_update_elem(fd, &key, &config, BPF_ANY);
734 }