From 65258eb4299953749d04a2fe413f0a4ce7da34e9 Mon Sep 17 00:00:00 2001 From: gbucchino Date: Thu, 16 Jan 2025 16:44:27 +0100 Subject: [PATCH] Update project --- dns-trace | Bin 17776 -> 0 bytes dns4.pcap | Bin 0 -> 5308 bytes src/common.h | 6 +++ src/dns-trace.ebpf.c | 119 ++++++++++++++++++++++++++++++++++++------- src/dns-trace.ebpf.o | Bin 28800 -> 0 bytes 5 files changed, 106 insertions(+), 19 deletions(-) delete mode 100755 dns-trace create mode 100644 dns4.pcap delete mode 100644 src/dns-trace.ebpf.o diff --git a/dns-trace b/dns-trace deleted file mode 100755 index 42329c5ac91e1ead2cdfcd495df3decb0a4d420c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17776 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 literal 0 HcmV?d00001 diff --git a/src/common.h b/src/common.h index 5af0e01..5c78665 100644 --- a/src/common.h +++ b/src/common.h @@ -28,6 +28,12 @@ struct event { char ans[32]; }; +struct query_section{ + char qname[QNAME_SIZE]; + int class; + int type; +}; + struct dns_answer { uint16_t tid; char qname[QNAME_SIZE]; diff --git a/src/dns-trace.ebpf.c b/src/dns-trace.ebpf.c index a3e46b8..c595cfd 100644 --- a/src/dns-trace.ebpf.c +++ b/src/dns-trace.ebpf.c @@ -35,22 +35,59 @@ struct { __type(value, struct dns_answer); } m_tid SEC(".maps"); +static size_t get_labels(struct __sk_buff *skb, size_t offset, size_t end, struct event *s_event, struct query_section *s_query){ + size_t len; + unsigned char buf[256]; + unsigned char *c; + int index = 0; + int qname_len = 0; // Full length of the qname field + + bpf_skb_load_bytes(skb, offset, &buf, 41); + c = buf; + + while (*(c++) != '\0') { + if(*c >= 'a' && *c <= 'z') + s_event->qname[index] = *c; + else if(*c >= 'A' && *c <= 'Z') + s_event->qname[index] = *c; + else + s_event->qname[index] = '.'; + index++; + qname_len++; + } + s_event->qname[--index] = '\0'; + qname_len++; // For the null character + + return len; +} /* * This function get the query field and the return the length of it */ -static size_t get_query(struct __sk_buff *skb, struct event *s_event, uint16_t *class, uint16_t *type, size_t tlen){ +//static size_t get_query_section(struct __sk_buff *skb, struct event *s_event, uint16_t *class, uint16_t *type, uint8_t tlen){ +static size_t get_query_section(struct __sk_buff *skb, struct event *s_event, struct query_section *s_query, uint8_t offset){ size_t len; - char buf[QNAME_SIZE] = {0}; + char buf[256] = {0}; int index = 0; int qname_len = 0; // Full length of the qname field char qname[QNAME_SIZE] = {0}; char *c; + uint8_t flen = skb->len; + /* + We get the size for the buffer + We substract the full size of the buffer (skb->len) with the sizes of each headers (eth + ip + udp + dns header) + */ + uint8_t l = flen - offset; + + if (l < 0) + return 0; - - bpf_skb_load_bytes(skb, tlen, &buf, 41); + //bpf_skb_load_bytes(skb, tlen, &buf, l); + bpf_skb_load_bytes(skb, offset, &buf, 41); c = buf; + // get_labels(struct __sk_buff *skb, size_t offset, size_t end, struct event *s_event, struct query_section *s_query) + // get_labels(skb, offset, 41, s_event, s_query); /* * The qname is composed by a the number of bytes then follow by the label * For instance, for the qname www.bucchino.org, @@ -72,18 +109,35 @@ static size_t get_query(struct __sk_buff *skb, struct event *s_event, uint16_t * } s_event->qname[--index] = '\0'; qname_len++; // For the null character - bpf_printk("%s (%d) %d", s_event->qname, index, qname_len); + // bpf_printk("l: %d", l); // Get class and type len = qname_len; - bpf_skb_load_bytes(skb, tlen + qname_len, type, sizeof(uint16_t)); + bpf_skb_load_bytes(skb, offset + qname_len, &s_query->type, sizeof(uint16_t)); len += 2; - bpf_skb_load_bytes(skb, tlen + qname_len + 2, class, sizeof(uint16_t)); + bpf_skb_load_bytes(skb, offset + qname_len + 2, &s_query->class, sizeof(uint16_t)); len += 2; return len; } +static size_t get_answer(struct __sk_buff *skb, struct event *s_event, size_t tlen){ + size_t len = 0; + unsigned char buf[256] = {0}; // Need to be unsigned, otherwise, the result is fffff + if(bpf_skb_load_bytes(skb, tlen, &buf, 2) < 0) + return 0; + /* + * According to the RFC 1035 (https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4) + * In the section 4.1.4, message compression, the first two bits are set at 11 (0xc), + * that's means, it's a pointer. + * For instance, the two bytes 0xc00c, 0xc it's the pointer and 0x00c is the position in the DNS header + */ + if (buf[0] == 0xc0){ + bpf_printk("Pointer to %x", buf[1]); + } + + return len; +} /* * https://datatracker.ietf.org/doc/html/rfc1035 */ @@ -91,8 +145,7 @@ static int dnsquery(struct __sk_buff *skb, struct ethhdr eth, struct iphdr ip, s struct event *s_event; struct dnshdr dns = {0}; char saddr[32]; - uint16_t class, type; - //struct dns_answer s_dnsanswer; + struct query_section s_query = {0}; // bpf_printk("udp len: %d", ntohs(udp.len)); s_event = bpf_ringbuf_reserve(&m_data, sizeof(*s_event), 0); @@ -105,6 +158,15 @@ static int dnsquery(struct __sk_buff *skb, struct ethhdr eth, struct iphdr ip, s /* Get DNS header */ bpf_skb_load_bytes(skb, sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr), &dns, sizeof(struct dnshdr)); + // Check OpCode + uint16_t flags = ntohs(dns.flags); + uint16_t qr = flags & 0xF000; // Get the QR code: 0 -> query, 1 -> response + if (qr == 0x0) + bpf_printk("Query"); + else if(qr == 0x8000) + bpf_printk("Response"); + bpf_printk("Flags: %x %x", flags, qr); + if (ntohs(dns.nbQuestions) == 0){ bpf_ringbuf_discard(s_event, 0); return 0; @@ -123,14 +185,15 @@ static int dnsquery(struct __sk_buff *skb, struct ethhdr eth, struct iphdr ip, s /* Get the query structure */ - size_t tlen = sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(struct dnshdr); - size_t query_len = get_query(skb, s_event, &class, &type, tlen); + uint8_t tlen = sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(struct dnshdr); + // size_t query_len = get_query_section(skb, s_event, &class, &type, tlen); + size_t query_len = get_query_section(skb, s_event, &s_query, tlen); // https://docs.cilium.io/en/stable/reference-guides/bpf/progtypes/ s_event->dport = dport; s_event->sport = sport; - s_event->class = ntohs(class); - s_event->type = ntohs(type); + s_event->class = ntohs(s_query.class); + s_event->type = ntohs(s_query.type); //if(bpf_probe_read_user_str(&s_event->qname, sizeof(s_event->qname), qname) < 0) // bpf_printk("Failed to copy qname"); @@ -146,14 +209,26 @@ static int dnsanswer(struct __sk_buff *skb, struct ethhdr eth, struct iphdr ip, struct dnshdr dns = {0}; uint16_t tid = 0; struct dns_answer s_dnsanswer; - - /* Get DNS header */ - bpf_skb_load_bytes(skb, sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr), &dns, sizeof(struct dnshdr)); + struct query_section s_query = {0}; s_event = bpf_ringbuf_reserve(&m_data, sizeof(*s_event), 0); if (!s_event) return 0; + /* Get DNS header */ + bpf_skb_load_bytes(skb, sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr), &dns, sizeof(struct dnshdr)); + + // Check OpCode + uint16_t flags = ntohs(dns.flags); + uint16_t qr = flags & 0xF000; // Get the QR code: 0 -> query, 1 -> response + if (qr == 0x0){} // Query + else if(qr == 0x8000){} // Response + + if (ntohs(dns.nbQuestions) == 0 && ntohs(dns.nbAnswerRRs)){ + bpf_ringbuf_discard(s_event, 0); + return 0; + } + /* Get the Transaction ID */ tid = ntohs(dns.transactionID); bpf_printk("tid: %x", tid); @@ -176,13 +251,19 @@ static int dnsanswer(struct __sk_buff *skb, struct ethhdr eth, struct iphdr ip, */ /* Get the query response */ - size_t tlen = sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(struct dnshdr); + uint8_t tlen = sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(struct dnshdr); uint16_t class, type; - size_t query_len = get_query(skb, s_event, &class, &type, tlen); + // size_t query_len = get_query_section(skb, s_event, &class, &type, tlen); + size_t query_len = get_query_section(skb, s_event, &s_query, tlen); bpf_printk("answer qname: %s", s_event->qname); + s_event->dport = dport; + s_event->sport = sport; + s_event->class = ntohs(class); + s_event->type = ntohs(type); /* Get the answer */ - + tlen += query_len; + size_t answer_len = get_answer(skb, s_event, tlen); bpf_ringbuf_submit(s_event, 0); return 0; diff --git a/src/dns-trace.ebpf.o b/src/dns-trace.ebpf.o deleted file mode 100644 index bdd94b2ab710c583415747b83669e57fee595a4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28800 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