From 6ff06d66c36c52e321f5d2ead5b15fa3215914e0 Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Fri, 4 Mar 2022 15:01:53 +0100 Subject: [PATCH] dns: add code for snooping dns packets This makes dns entries work in bridged mode or when not using dnsmasq Signed-off-by: Felix Fietkau --- CMakeLists.txt | 2 +- dns.c | 414 +++++++++++++++++++++++++++++++++++++++++++++++++ interface.c | 16 +- main.c | 2 + map.c | 2 +- qosify.h | 6 + 6 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 dns.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d73a60..3282866 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ ADD_DEFINITIONS(-Os -Wall -Wno-unknown-warning-option -Wno-array-bounds -Wno-for SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") find_library(bpf NAMES bpf) -ADD_EXECUTABLE(qosify main.c loader.c map.c ubus.c interface.c) +ADD_EXECUTABLE(qosify main.c loader.c map.c ubus.c interface.c dns.c) TARGET_LINK_LIBRARIES(qosify ${bpf} ubox ubus) INSTALL(TARGETS qosify diff --git a/dns.c b/dns.c new file mode 100644 index 0000000..1706238 --- /dev/null +++ b/dns.c @@ -0,0 +1,414 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define FLAG_RESPONSE 0x8000 +#define FLAG_OPCODE 0x7800 +#define FLAG_AUTHORATIVE 0x0400 +#define FLAG_RCODE 0x000f + +#define TYPE_A 0x0001 +#define TYPE_CNAME 0x0005 +#define TYPE_PTR 0x000c +#define TYPE_TXT 0x0010 +#define TYPE_AAAA 0x001c +#define TYPE_SRV 0x0021 +#define TYPE_ANY 0x00ff + +#define IS_COMPRESSED(x) ((x & 0xc0) == 0xc0) + +#define CLASS_FLUSH 0x8000 +#define CLASS_UNICAST 0x8000 +#define CLASS_IN 0x0001 + +#define MAX_NAME_LEN 256 +#define MAX_DATA_LEN 8096 + +#include "qosify.h" + +static struct uloop_fd ufd; +static struct uloop_timeout cname_gc_timer; +static AVL_TREE(cname_cache, avl_strcmp, false, NULL); + +struct vlan_hdr { + uint16_t tci; + uint16_t proto; +}; + +struct packet { + void *buffer; + unsigned int len; +}; + +struct dns_header { + uint16_t id; + uint16_t flags; + uint16_t questions; + uint16_t answers; + uint16_t authority; + uint16_t additional; +} __packed; + +struct dns_question { + uint16_t type; + uint16_t class; +} __packed; + +struct dns_answer { + uint16_t type; + uint16_t class; + uint32_t ttl; + uint16_t rdlength; +} __packed; + +struct cname_entry { + struct avl_node node; + uint8_t dscp; + uint8_t age; +}; + +static void *pkt_peek(struct packet *pkt, unsigned int len) +{ + if (len > pkt->len) + return NULL; + + return pkt->buffer; +} + + +static void *pkt_pull(struct packet *pkt, unsigned int len) +{ + void *ret = pkt_peek(pkt, len); + + if (!ret) + return NULL; + + pkt->buffer += len; + pkt->len -= len; + + return ret; +} + +static int pkt_pull_name(struct packet *pkt, const void *hdr, char *dest) +{ + int len; + + if (dest) + len = dn_expand(hdr, pkt->buffer + pkt->len, pkt->buffer, + (void *)dest, MAX_NAME_LEN); + else + len = dn_skipname(pkt->buffer, pkt->buffer + pkt->len - 1); + + if (len < 0 || !pkt_pull(pkt, len)) + return -1; + + return 0; +} + +static bool +proto_is_vlan(uint16_t proto) +{ + return proto == ETH_P_8021Q || proto == ETH_P_8021AD; +} + +static void +cname_cache_set(const char *name, uint8_t dscp) +{ + struct cname_entry *e; + + e = avl_find_element(&cname_cache, name, e, node); + if (!e) { + char *name_buf; + + e = calloc_a(sizeof(*e), &name_buf, strlen(name) + 1); + e->node.key = strcpy(name_buf, name); + avl_insert(&cname_cache, &e->node); + } + + e->age = 0; + e->dscp = dscp; +} + +static int +cname_cache_get(const char *name, uint8_t *dscp) +{ + struct cname_entry *e; + + e = avl_find_element(&cname_cache, name, e, node); + if (!e) + return -1; + + *dscp = e->dscp; + return 0; +} + +static int +dns_parse_question(struct packet *pkt, const void *hdr, uint8_t *dscp) +{ + char qname[MAX_NAME_LEN]; + + if (pkt_pull_name(pkt, hdr, qname) || + !pkt_pull(pkt, sizeof(struct dns_question))) + return -1; + + cname_cache_get(qname, dscp); + qosify_map_lookup_dns_entry(qname, dscp); + + return 0; +} + +static int +dns_parse_answer(struct packet *pkt, void *hdr, uint8_t *dscp) +{ + struct qosify_map_data data = {}; + char cname[MAX_NAME_LEN]; + struct dns_answer *a; + int prev_timeout; + void *rdata; + int len; + + if (pkt_pull_name(pkt, hdr, NULL)) + return -1; + + a = pkt_pull(pkt, sizeof(*a)); + if (!a) + return -1; + + len = be16_to_cpu(a->rdlength); + rdata = pkt_pull(pkt, len); + if (!rdata) + return -1; + + switch (be16_to_cpu(a->type)) { + case TYPE_CNAME: + if (dn_expand(hdr, pkt->buffer + pkt->len, rdata, + cname, sizeof(cname)) < 0) + return -1; + + qosify_map_lookup_dns_entry(cname, dscp); + cname_cache_set(cname, *dscp); + + return 0; + case TYPE_A: + data.id = CL_MAP_IPV4_ADDR; + memcpy(&data.addr, rdata, 4); + break; + case TYPE_AAAA: + data.id = CL_MAP_IPV6_ADDR; + memcpy(&data.addr, rdata, 16); + break; + default: + return 0; + } + + data.user = true; + data.dscp = *dscp; + + prev_timeout = qosify_map_timeout; + qosify_map_timeout = be32_to_cpu(a->ttl); + __qosify_map_set_entry(&data); + qosify_map_timeout = prev_timeout; + + return 0; +} + +static void +qosify_dns_data_cb(struct packet *pkt) +{ + struct dns_header *h; + uint8_t dscp = 0xff; + int i; + + h = pkt_pull(pkt, sizeof(*h)); + if (!h) + return; + + if ((h->flags & cpu_to_be16(FLAG_RESPONSE | FLAG_OPCODE | FLAG_RCODE)) != + cpu_to_be16(FLAG_RESPONSE)) + return; + + if (h->questions != cpu_to_be16(1)) + return; + + if (dns_parse_question(pkt, h, &dscp)) + return; + + for (i = 0; i < be16_to_cpu(h->answers); i++) + if (dns_parse_answer(pkt, h, &dscp)) + return; +} + +static void +qosify_dns_packet_cb(struct packet *pkt) +{ + struct ethhdr *eth; + struct ip6_hdr *ip6; + struct ip *ip; + uint16_t proto; + + eth = pkt_pull(pkt, sizeof(*eth)); + if (!eth) + return; + + proto = be16_to_cpu(eth->h_proto); + if (proto_is_vlan(proto)) { + struct vlan_hdr *vlan; + + vlan = pkt_pull(pkt, sizeof(*vlan)); + if (!vlan) + return; + + proto = vlan->proto; + } + + switch (proto) { + case ETH_P_IP: + ip = pkt_peek(pkt, sizeof(struct ip)); + if (!ip) + return; + + if (!pkt_pull(pkt, ip->ip_hl * 4)) + return; + + proto = ip->ip_p; + break; + case ETH_P_IPV6: + ip6 = pkt_pull(pkt, sizeof(*ip6)); + if (!ip6) + return; + + proto = ip6->ip6_nxt; + break; + default: + return; + } + + if (proto != IPPROTO_UDP) + return; + + if (!pkt_pull(pkt, sizeof(struct udphdr))) + return; + + qosify_dns_data_cb(pkt); +} + +static void +qosify_dns_socket_cb(struct uloop_fd *fd, unsigned int events) +{ + static uint8_t buf[8192]; + struct packet pkt = { + .buffer = buf, + }; + int len; + +retry: + len = recvfrom(fd->fd, buf, sizeof(buf), MSG_DONTWAIT, NULL, NULL); + if (len < 0) { + if (errno == EINTR) + goto retry; + return; + } + + if (!len) + return; + + pkt.len = len; + qosify_dns_packet_cb(&pkt); +} + +static void +qosify_cname_cache_gc(struct uloop_timeout *timeout) +{ + struct cname_entry *e, *tmp; + + avl_for_each_element_safe(&cname_cache, e, node, tmp) { + if (e->age++ < 5) + continue; + + avl_delete(&cname_cache, &e->node); + free(e); + } + + uloop_timeout_set(timeout, 1000); +} + +static int +qosify_open_dns_socket(void) +{ + struct sockaddr_ll sll = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_ALL), + }; + int sock; + + sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (sock == -1) { + ULOG_ERR("failed to create raw socket: %s\n", strerror(errno)); + return -1; + } + + sll.sll_ifindex = if_nametoindex(QOSIFY_DNS_IFNAME); + if (bind(sock, (struct sockaddr *)&sll, sizeof(sll))) { + ULOG_ERR("failed to bind socket to "QOSIFY_DNS_IFNAME": %s\n", + strerror(errno)); + goto error; + } + + ufd.fd = sock; + ufd.cb = qosify_dns_socket_cb; + uloop_fd_add(&ufd, ULOOP_READ); + + return 0; + +error: + close(sock); + return -1; +} + +static void +qosify_dns_del_ifb(void) +{ + qosify_run_cmd("ip link del ifb-dns type ifb", true); +} + +int qosify_dns_init(void) +{ + cname_gc_timer.cb = qosify_cname_cache_gc; + qosify_cname_cache_gc(&cname_gc_timer); + + qosify_dns_del_ifb(); + + if (qosify_run_cmd("ip link add ifb-dns type ifb", false) || + qosify_run_cmd("ip link set dev ifb-dns up", false) || + qosify_open_dns_socket()) + return -1; + + return 0; +} + +void qosify_dns_stop(void) +{ + struct cname_entry *e, *tmp; + + if (ufd.registered) { + uloop_fd_delete(&ufd); + close(ufd.fd); + } + + qosify_dns_del_ifb(); + + avl_remove_all_elements(&cname_cache, e, node, tmp) + free(e); +} + diff --git a/interface.c b/interface.c index b6e2895..7d9cecd 100644 --- a/interface.c +++ b/interface.c @@ -275,6 +275,19 @@ cmd_add_ingress(struct qosify_iface *iface, bool eth) ofs = prepare_tc_cmd(buf, sizeof(buf), "qdisc", "add", iface->ifname, " handle ffff: ingress"); qosify_run_cmd(buf, false); + ofs = prepare_tc_cmd(buf, sizeof(buf), "filter", "add", iface->ifname, " parent ffff:"); + APPEND(buf, ofs, " protocol ip prio 5 u32 match ip sport 53 0xffff " + "flowid 1:1 action mirred egress redirect dev ifb-dns"); + qosify_run_cmd(buf, false); + + ofs = prepare_tc_cmd(buf, sizeof(buf), "filter", "add", iface->ifname, " parent ffff:"); + APPEND(buf, ofs, " protocol ipv6 prio 6 u32 match ip6 sport 53 0xffff " + "flowid 1:1 action mirred egress redirect dev ifb-dns"); + qosify_run_cmd(buf, false); + + if (!iface->config.ingress) + return 0; + snprintf(buf, sizeof(buf), "ip link add '%s' type ifb", ifbdev); qosify_run_cmd(buf, false); @@ -310,8 +323,7 @@ interface_start(struct qosify_iface *iface) if (iface->config.egress) cmd_add_qdisc(iface, iface->ifname, true, eth); - if (iface->config.ingress) - cmd_add_ingress(iface, eth); + cmd_add_ingress(iface, eth); iface->active = true; } diff --git a/main.c b/main.c index 6e73c8f..56b4a30 100644 --- a/main.c +++ b/main.c @@ -121,6 +121,8 @@ int main(int argc, char **argv) qosify_iface_init()) return 2; + qosify_dns_init(); + uloop_run(); qosify_ubus_stop(); diff --git a/map.c b/map.c index c9c75aa..436de60 100644 --- a/map.c +++ b/map.c @@ -284,7 +284,7 @@ __qosify_map_alloc_entry(struct qosify_map_data *data) return e; } -static void __qosify_map_set_entry(struct qosify_map_data *data) +void __qosify_map_set_entry(struct qosify_map_data *data) { int fd = qosify_map_fds[data->id]; struct qosify_map_entry *e; diff --git a/qosify.h b/qosify.h index 52edd9b..4654385 100644 --- a/qosify.h +++ b/qosify.h @@ -24,6 +24,8 @@ #define CLASSIFY_PIN_PATH "/sys/fs/bpf/qosify" #define CLASSIFY_DATA_PATH "/sys/fs/bpf/qosify_data" +#define QOSIFY_DNS_IFNAME "ifb-dns" + enum qosify_map_id { CL_MAP_TCP_PORTS, CL_MAP_UDP_PORTS, @@ -76,6 +78,7 @@ int qosify_loader_init(void); int qosify_map_init(void); int qosify_map_dscp_value(const char *val, uint8_t *dscp); int qosify_map_load_file(const char *file); +void __qosify_map_set_entry(struct qosify_map_data *data); int qosify_map_set_entry(enum qosify_map_id id, bool file, const char *str, uint8_t dscp); void qosify_map_reload(void); @@ -98,6 +101,9 @@ void qosify_iface_check(void); void qosify_iface_status(struct blob_buf *b); void qosify_iface_stop(void); +int qosify_dns_init(void); +void qosify_dns_stop(void); + int qosify_ubus_init(void); void qosify_ubus_stop(void); int qosify_ubus_check_interface(const char *name, char *ifname, int ifname_len); -- 2.30.2