dns: add code for snooping dns packets
authorFelix Fietkau <nbd@nbd.name>
Fri, 4 Mar 2022 14:01:53 +0000 (15:01 +0100)
committerFelix Fietkau <nbd@nbd.name>
Sun, 6 Mar 2022 15:02:03 +0000 (16:02 +0100)
This makes dns entries work in bridged mode or when not using dnsmasq

Signed-off-by: Felix Fietkau <nbd@nbd.name>
CMakeLists.txt
dns.c [new file with mode: 0644]
interface.c
main.c
map.c
qosify.h

index 3d73a60a0fbd84bb8f96dfed74a14b3dfaf3dbb3..328286688affb3e373d2bc22e8fcd8334c5f8883 100644 (file)
@@ -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 (file)
index 0000000..1706238
--- /dev/null
+++ b/dns.c
@@ -0,0 +1,414 @@
+#include <netinet/if_ether.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/udp.h>
+#include <netpacket/packet.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <resolv.h>
+
+#include <libubox/uloop.h>
+#include <libubox/avl-cmp.h>
+
+#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);
+}
+
index b6e2895c30b9eb267876913a28c6564bedd654d0..7d9cecd76d11e205c60749a0063e07071ffc37cb 100644 (file)
@@ -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 6e73c8f2c6981882e0b33e1e242bc5bc37bfe273..56b4a3039329a0609575fc21cea43ca20d5c5646 100644 (file)
--- 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 c9c75aa1633731f611f09d5cd6c6034a7cba14a2..436de60ded8ace0aecc6004e4752dd6e0d48ea5c 100644 (file)
--- 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;
index 52edd9b0e318949eabcfb5808c51efa13ed5ea44..46543852072e9dc3d0b7d958d591c6d199e15c89 100644 (file)
--- 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);