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