summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFelix Fietkau2022-03-04 14:01:53 +0000
committerFelix Fietkau2022-03-06 15:02:03 +0000
commit6ff06d66c36c52e321f5d2ead5b15fa3215914e0 (patch)
tree11e6aebe0b97f89a46ba174516e161a260ee6a42
parent558eabc13c64bdbd2d380e9520d56107d60a13cd (diff)
downloadqosify-6ff06d66c36c52e321f5d2ead5b15fa3215914e0.tar.gz
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 <nbd@nbd.name>
-rw-r--r--CMakeLists.txt2
-rw-r--r--dns.c414
-rw-r--r--interface.c16
-rw-r--r--main.c2
-rw-r--r--map.c2
-rw-r--r--qosify.h6
6 files changed, 438 insertions, 4 deletions
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 <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);
+}
+
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);