From: Pierre GriĆ© Date: Mon, 22 Nov 2021 16:56:12 +0000 (+0100) Subject: dnsdist: Added XDP middleware for dropped/redirected queries logging X-Git-Tag: dnsdist-1.8.0-rc1~207^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4b2ade29644d01f9cf569b7dfdb6047d094e302a;p=thirdparty%2Fpdns.git dnsdist: Added XDP middleware for dropped/redirected queries logging --- diff --git a/contrib/xdp-filter.ebpf.src b/contrib/xdp-filter.ebpf.src index 5e4d9d13b8..879baee3a4 100644 --- a/contrib/xdp-filter.ebpf.src +++ b/contrib/xdp-filter.ebpf.src @@ -117,6 +117,7 @@ struct map_value BPF_TABLE_PINNED("hash", uint32_t, struct map_value, v4filter, 1024, "/sys/fs/bpf/dnsdist/addr-v4"); BPF_TABLE_PINNED("hash", struct in6_addr, struct map_value, v6filter, 1024, "/sys/fs/bpf/dnsdist/addr-v6"); BPF_TABLE_PINNED("hash", struct dns_qname, struct map_value, qnamefilter, 1024, "/sys/fs/bpf/dnsdist/qnames"); +BPF_TABLE_PINNED("prog", int, int, progsarray, 2, "/sys/fs/bpf/dnsdist/progs"); /* * bcc has added BPF_TABLE_PINNED7 to the latest commit of the master branch, but it has not yet been released. @@ -430,7 +431,11 @@ int xdp_dns_filter(struct xdp_md* ctx) key.addr = bpf_htonl(ipv4->saddr); // if TC bit must not be set, apply the action if ((r = udp_dns_reply_v4(&c, &key)) != TC) { - return r == DROP ? XDP_DROP : XDP_PASS; + if (r == DROP) { + progsarray.call(ctx, 0); + return XDP_DROP; + } + return XDP_PASS; } // swap src/dest IP addresses @@ -448,7 +453,11 @@ int xdp_dns_filter(struct xdp_md* ctx) // if TC bit must not be set, apply the action if ((r = udp_dns_reply_v6(&c, &key)) != TC) { - return r == DROP ? XDP_DROP : XDP_PASS; + if (r == DROP) { + progsarray.call(ctx, 0); + return XDP_DROP; + } + return XDP_PASS; } // swap src/dest IP addresses @@ -471,6 +480,8 @@ int xdp_dns_filter(struct xdp_md* ctx) memcpy(eth->h_dest, eth->h_source, ETH_ALEN); memcpy(eth->h_source, swap_eth, ETH_ALEN); + progsarray.call(ctx, 1); + // bounce the request return XDP_TX; } diff --git a/contrib/xdp-logging-middleware.ebpf.src b/contrib/xdp-logging-middleware.ebpf.src new file mode 100644 index 0000000000..fee572992b --- /dev/null +++ b/contrib/xdp-logging-middleware.ebpf.src @@ -0,0 +1,274 @@ +#include +#include +#include +#include + +#define DNS_PORT 53 + +// do not use libc includes because this causes clang +// to include 32bit headers on 64bit ( only ) systems. +typedef __u8 uint8_t; +typedef __u16 uint16_t; +typedef __u32 uint32_t; +typedef __u64 uint64_t; +#define memcpy __builtin_memcpy + +/* + * Helper pointer to parse the incoming packets + * Copyright 2020, NLnet Labs, All rights reserved. + */ +struct cursor { + void *pos; + void *end; +}; + +/* + * Store the VLAN header + * Copyright 2020, NLnet Labs, All rights reserved. + */ +struct vlanhdr { + uint16_t tci; + uint16_t encap_proto; +}; + +/* + * Store the DNS header + * Copyright 2020, NLnet Labs, All rights reserved. + */ +struct dnshdr { + uint16_t id; + union { + struct { +#if BYTE_ORDER == LITTLE_ENDIAN + uint8_t rd : 1; + uint8_t tc : 1; + uint8_t aa : 1; + uint8_t opcode : 4; + uint8_t qr : 1; + + uint8_t rcode : 4; + uint8_t cd : 1; + uint8_t ad : 1; + uint8_t z : 1; + uint8_t ra : 1; +#elif BYTE_ORDER == BIG_ENDIAN || BYTE_ORDER == PDP_ENDIAN + uint8_t qr : 1; + uint8_t opcode : 4; + uint8_t aa : 1; + uint8_t tc : 1; + uint8_t rd : 1; + + uint8_t ra : 1; + uint8_t z : 1; + uint8_t ad : 1; + uint8_t cd : 1; + uint8_t rcode : 4; +#endif + } as_bits_and_pieces; + uint16_t as_value; + } flags; + uint16_t qdcount; + uint16_t ancount; + uint16_t nscount; + uint16_t arcount; +}; + +/* + * Store the qname and qtype + */ +struct dns_qname +{ + uint8_t qname[255]; + uint16_t qtype; +}; + +/* + * The possible actions to perform on the packet + * PASS: XDP_PASS + * DROP: XDP_DROP + * TC: set TC bit and XDP_TX + */ +enum dns_action { + PASS = 0, + DROP = 1, + TC = 2 +}; + +BPF_TABLE_PINNED("prog", int, int, progsarray, 2, "/sys/fs/bpf/dnsdist/progs"); +BPF_PERF_OUTPUT(events); + +/* + * Initializer of a cursor pointer + * Copyright 2020, NLnet Labs, All rights reserved. + */ +static inline void cursor_init(struct cursor *c, struct xdp_md *ctx) +{ + c->end = (void *)(long)ctx->data_end; + c->pos = (void *)(long)ctx->data; +} + +/* + * Header parser functions + * Copyright 2020, NLnet Labs, All rights reserved. + */ +#define PARSE_FUNC_DECLARATION(STRUCT) \ +static inline struct STRUCT *parse_ ## STRUCT (struct cursor *c) \ +{ \ + struct STRUCT *ret = c->pos; \ + if (c->pos + sizeof(struct STRUCT) > c->end) \ + return 0; \ + c->pos += sizeof(struct STRUCT); \ + return ret; \ +} + +PARSE_FUNC_DECLARATION(ethhdr) +PARSE_FUNC_DECLARATION(vlanhdr) +PARSE_FUNC_DECLARATION(iphdr) +PARSE_FUNC_DECLARATION(ipv6hdr) +PARSE_FUNC_DECLARATION(udphdr) +PARSE_FUNC_DECLARATION(dnshdr) + +/* + * Parse ethernet frame and fill the struct + * Copyright 2020, NLnet Labs, All rights reserved. + */ +static inline struct ethhdr *parse_eth(struct cursor *c, uint16_t *eth_proto) +{ + struct ethhdr *eth; + + if (!(eth = parse_ethhdr(c))) + return 0; + + *eth_proto = eth->h_proto; + if (*eth_proto == bpf_htons(ETH_P_8021Q) + || *eth_proto == bpf_htons(ETH_P_8021AD)) { + struct vlanhdr *vlan; + + if (!(vlan = parse_vlanhdr(c))) + return 0; + + *eth_proto = vlan->encap_proto; + if (*eth_proto == bpf_htons(ETH_P_8021Q) + || *eth_proto == bpf_htons(ETH_P_8021AD)) { + if (!(vlan = parse_vlanhdr(c))) + return 0; + + *eth_proto = vlan->encap_proto; + } + } + return eth; +} + +/* + * Parse DNS QName and fill the struct + */ +static inline void parse_qname(struct cursor *c, struct dns_qname *query) +{ + uint8_t qname_byte; + uint16_t qtype; + int length = 0; + + for(int i = 0; i<255; i++) { + bpf_probe_read_kernel(&qname_byte, sizeof(qname_byte), c->pos); + + c->pos += 1; + if (length == 0) { + if (qname_byte == 0 || qname_byte > 63 ) { + break; + } + length += qname_byte; + } else { + length--; + } + if (qname_byte >= 'A' && qname_byte <= 'Z') { + query->qname[i] = qname_byte + ('a' - 'A'); + } else { + query->qname[i] = qname_byte; + } + } + + bpf_probe_read_kernel(&(query->qtype), sizeof(query->qtype), c->pos); +} + +/* + * Push data regarding the dropped/redirected packet to a perf buffer + */ +static inline void log_packet(struct xdp_md *ctx, enum dns_action action) { + // store variables + struct cursor c; + struct ethhdr *eth; + uint16_t eth_proto; + struct iphdr *ipv4; + struct ipv6hdr *ipv6; + struct udphdr *udp; + struct dnshdr *dns; + int r = 0; + + struct pktdata { + uint32_t ipv4_src; + uint8_t ipv6_src[16]; + struct dns_qname query; + } packet_info = {0}; + + // initialise the cursor + cursor_init(&c, ctx); + + if ((eth = parse_eth(&c, ð_proto))) { + if (eth_proto == bpf_htons(ETH_P_IP)) + { + if ((ipv4 = parse_iphdr(&c))) + { + if (action == DROP) + { + memcpy(&(packet_info.ipv4_src), &(ipv4->saddr), sizeof(packet_info.ipv4_src)); + } + else if (action == TC) + { + memcpy(&(packet_info.ipv4_src), &(ipv4->daddr), sizeof(packet_info.ipv4_src)); + } + if ((udp = parse_udphdr(&c))) + { + if ((dns = parse_dnshdr(&c))) + { + parse_qname(&c, &(packet_info.query)); + } + } + } + + } + else if (eth_proto == bpf_htons(ETH_P_IPV6)) + { + if ((ipv6 = parse_ipv6hdr(&c))) + { + if (action == DROP) + { + memcpy(&(packet_info.ipv6_src), &(ipv6->saddr.in6_u.u6_addr8), 16); + } + else if (action == TC) + { + memcpy(&(packet_info.ipv6_src), &(ipv6->daddr.in6_u.u6_addr8), 16); + } + if ((udp = parse_udphdr(&c))) + { + if ((dns = parse_dnshdr(&c))) + { + parse_qname(&c, &(packet_info.query)); + } + } + } + } + } + events.perf_submit(ctx, &packet_info, sizeof(packet_info)); +} + +int log_drop(struct xdp_md *ctx) +{ + log_packet(ctx, DROP); + return XDP_DROP; +} + +int log_tc(struct xdp_md *ctx) +{ + log_packet(ctx, TC); + return XDP_TX; +} diff --git a/contrib/xdp-logging.py b/contrib/xdp-logging.py new file mode 100644 index 0000000000..1c0e012dee --- /dev/null +++ b/contrib/xdp-logging.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +from bcc import BPF +import ctypes as ct +import netaddr +import socket + +class DNSQuery(ct.Structure): + _fields_ = [ + ("qname", ct.c_uint8 * 255), + ("qtype", ct.c_uint16) + ] + +class PacketInfo(ct.Structure): + _fields_ = [ + ("ipv4_src", ct.c_uint32), + ("ipv6_src", ct.c_uint8 * 16), + ("query", DNSQuery) + ] + +def decode_qname(qname_array): + qname = "" + length = 0 + for qname_byte in qname_array: + if length == 0: + if int(qname_byte) == 0: + break + else: + length = int(qname_byte) + if qname != "": + qname += '.' + else: + qname += chr(int(qname_byte)) + length -= 1 + return qname + +def print_event(cpu, data, size): + event = ct.cast(data, ct.POINTER(PacketInfo)).contents + if event.ipv4_src != 0: + src_ip = str(netaddr.IPAddress(socket.htonl(event.ipv4_src))) + else: + src_ip = str(netaddr.IPAddress(sum([byte << 8*(15-index) for index, byte in enumerate(event.ipv6_src)]), 6)) + qtype = INV_QTYPES[socket.htons(event.query.qtype)] + qname = decode_qname(event.query.qname) + print(f"{src_ip}|{qtype}|{qname}") + +QTYPES = {'LOC': 29, '*': 255, 'IXFR': 251, 'UINFO': 100, 'NSEC3': 50, 'AAAA': 28, 'CNAME': 5, 'MINFO': 14, 'EID': 31, 'GPOS': 27, 'X25': 19, 'HINFO': 13, 'CAA': 257, 'NULL': 10, 'DNSKEY': 48, 'DS': 43, 'ISDN': 20, 'SOA': 6, 'RP': 17, 'UID': 101, 'TALINK': 58, 'TKEY': 249, 'PX': 26, 'NSAP-PTR': 23, 'TXT': 16, 'IPSECKEY': 45, 'DNAME': 39, 'MAILA': 254, 'AFSDB': 18, 'SSHFP': 44, 'NS': 2, 'PTR': 12, 'SPF': 99, 'TA': 32768, 'A': 1, 'NXT': 30, 'AXFR': 252, 'RKEY': 57, 'KEY': 25, 'NIMLOC': 32, 'A6': 38, 'TLSA': 52, 'MG': 8, 'HIP': 55, 'NSEC': 47, 'GID': 102, 'SRV': 33, 'DLV': 32769, 'NSEC3PARAM': 51, 'UNSPEC': 103, 'TSIG': 250, 'ATMA': 34, 'RRSIG': 46, 'OPT': 41, 'MD': 3, 'NAPTR': 35, 'MF': 4, 'MB': 7, 'DHCID': 49, 'MX': 15, 'MAILB': 253, 'CERT': 37, 'NINFO': 56, 'APL': 42, 'MR': 9, 'SIG': 24, 'WKS': 11, 'KX': 36, 'NSAP': 22, 'RT': 21, 'SINK': 40} +INV_QTYPES = {v: k for k, v in QTYPES.items()} + +# Main +xdp = BPF(src_file="xdp-logging-middleware.ebpf.src") + +fn_drop = xdp.load_func("log_drop", BPF.XDP) +fn_tc = xdp.load_func("log_tc", BPF.XDP) + +progs = xdp.get_table("progsarray") +events = xdp.get_table("events") + +progs[ct.c_int(0)] = ct.c_int(fn_drop.fd) +progs[ct.c_int(1)] = ct.c_int(fn_tc.fd) + +events.open_perf_buffer(print_event) + +print("Filter is ready") +while True: + try: + xdp.perf_buffer_poll() + except KeyboardInterrupt: + break + +if progs[ct.c_int(0)]: + del(progs[ct.c_int(0)]) +if progs[ct.c_int(1)]: + del(progs[ct.c_int(1)])