From: Remi Gacogne Date: Fri, 29 Oct 2021 14:56:08 +0000 (+0200) Subject: dnsdist: Add a sample XDP program and associated python script in contrib X-Git-Tag: dnsdist-1.7.0-beta1^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=22d90864785984ddf22569c435b441a00dfd4012;p=thirdparty%2Fpdns.git dnsdist: Add a sample XDP program and associated python script in contrib Both contributed by Pierre Grié . --- diff --git a/contrib/xdp-filter.ebpf.src b/contrib/xdp-filter.ebpf.src new file mode 100644 index 0000000000..46e3123c11 --- /dev/null +++ b/contrib/xdp-filter.ebpf.src @@ -0,0 +1,430 @@ +#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 : uint32_t { + PASS = 0, + DROP = 1, + TC = 2 +}; + +/* + * Store the matching counter and the associated action for a blocked element + */ +struct map_value +{ + uint64_t counter; + enum dns_action action; +}; + +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"); + +/* + * 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; +} + +/* + * Recalculate the checksum + * Copyright 2020, NLnet Labs, All rights reserved. + */ +static inline void update_checksum(uint16_t *csum, uint16_t old_val, uint16_t new_val) +{ + uint32_t new_csum_value; + uint32_t new_csum_comp; + uint32_t undo; + + undo = ~((uint32_t)*csum) + ~((uint32_t)old_val); + new_csum_value = undo + (undo < ~((uint32_t)old_val)) + (uint32_t)new_val; + new_csum_comp = new_csum_value + (new_csum_value < ((uint32_t)new_val)); + new_csum_comp = (new_csum_comp & 0xFFFF) + (new_csum_comp >> 16); + new_csum_comp = (new_csum_comp & 0xFFFF) + (new_csum_comp >> 16); + *csum = (uint16_t)~new_csum_comp; +} + +/* + * Set the TC bit and swap UDP ports + * Copyright 2020, NLnet Labs, All rights reserved. + */ +static inline enum dns_action set_tc_bit(struct udphdr *udp, struct dnshdr *dns) +{ + uint16_t old_val = dns->flags.as_value; + + // change the DNS flags + dns->flags.as_bits_and_pieces.ad = 0; + dns->flags.as_bits_and_pieces.qr = 1; + dns->flags.as_bits_and_pieces.tc = 1; + + // change the UDP destination to the source + udp->dest = udp->source; + udp->source = bpf_htons(DNS_PORT); + + // calculate and write the new checksum + update_checksum(&udp->check, old_val, dns->flags.as_value); + + // bounce + return TC; +} + +/* + * Check DNS QName + * Returns PASS if message needs to go through (i.e. pass) + * TC if (modified) message needs to be replied + * DROP if message needs to be blocke + */ +static inline enum dns_action check_qname(struct cursor *c) +{ + struct dns_qname qkey = {0}; + uint8_t qname_byte; + uint16_t qtype; + int length = 0; + + for(int i = 0; i<255; i++) { + if (bpf_probe_read_kernel(&qname_byte, sizeof(qname_byte), c->pos)) { + return PASS; + } + 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') { + qkey.qname[i] = qname_byte + ('a' - 'A'); + } else { + qkey.qname[i] = qname_byte; + } + } + + // if the last read qbyte is not 0 incorrect QName format), return PASS + if (qname_byte != 0) { + return PASS; + } + + // get QType + if(bpf_probe_read_kernel(&qtype, sizeof(qtype), c->pos)) { + return PASS; + } + + struct map_value* value; + + // check if Qname/Qtype is blocked + qkey.qtype = bpf_htons(qtype); + value = qnamefilter.lookup(&qkey); + if (value) { + __sync_fetch_and_add(&value->counter, 1); + return value->action; + } + + // check with Qtype 255 (*) + qkey.qtype = 255; + + value = qnamefilter.lookup(&qkey); + if (value) { + __sync_fetch_and_add(&value->counter, 1); + return value->action; + } + + return PASS; +} + +/* + * Parse IPv4 DNS mesage. + * Returns PASS if message needs to go through (i.e. pass) + * TC if (modified) message needs to be replied + * DROP if message needs to be blocked + */ +static inline enum dns_action udp_dns_reply_v4(struct cursor *c, uint32_t key) +{ + struct udphdr *udp; + struct dnshdr *dns; + + if (!(udp = parse_udphdr(c)) || udp->dest != bpf_htons(DNS_PORT)) { + return PASS; + } + + // check that we have a DNS packet + if (!(dns = parse_dnshdr(c))) { + return PASS; + } + + // if the address is blocked, perform the corresponding action + struct map_value* value = v4filter.lookup(&key); + + if (value) { + __sync_fetch_and_add(&value->counter, 1); + if (value->action == TC) { + return set_tc_bit(udp, dns); + } else { + return value->action; + } + } else { + enum dns_action action = check_qname(c); + if (action == TC) { + return set_tc_bit(udp, dns); + } else { + return action; + } + } + + return PASS; +} + +/* + * Parse IPv6 DNS mesage. + * Returns PASS if message needs to go through (i.e. pass) + * TC if (modified) message needs to be replied + * DROP if message needs to be blocked + */ +static inline enum dns_action udp_dns_reply_v6(struct cursor *c, struct in6_addr key) +{ + struct udphdr *udp; + struct dnshdr *dns; + + + if (!(udp = parse_udphdr(c)) || udp->dest != bpf_htons(DNS_PORT)) { + return PASS; + } + + // check that we have a DNS packet + ; + if (!(dns = parse_dnshdr(c))) { + return PASS; + } + + // if the address is blocked, perform the corresponding action + struct map_value* value = v6filter.lookup(&key); + + if (value) { + __sync_fetch_and_add(&value->counter, 1); + if (value->action == TC) { + return set_tc_bit(udp, dns); + } else { + return value->action; + } + } else { + enum dns_action action = check_qname(c); + if (action == TC) { + return set_tc_bit(udp, dns); + } else { + return action; + } + } + + return PASS; +} + +int xdp_dns_filter(struct xdp_md *ctx) +{ + // store variables + struct cursor c; + struct ethhdr *eth; + uint16_t eth_proto; + struct iphdr *ipv4; + struct ipv6hdr *ipv6; + int r = 0; + + // initialise the cursor + cursor_init(&c, ctx); + + // pass the packet if it is not an ethernet one + if ((eth = parse_eth(&c, ð_proto))) { + // IPv4 packets + if (eth_proto == bpf_htons(ETH_P_IP)) + { + if (!(ipv4 = parse_iphdr(&c)) || bpf_htons(ipv4->protocol != IPPROTO_UDP)) { + return XDP_PASS; + } + // if TC bit must not be set, apply the action + if ((r = udp_dns_reply_v4(&c, bpf_htonl(ipv4->saddr))) != TC) { + return r == DROP ? XDP_DROP : XDP_PASS; + } + + // swap src/dest IP addresses + uint32_t swap_ipv4 = ipv4->daddr; + ipv4->daddr = ipv4->saddr; + ipv4->saddr = swap_ipv4; + } + // IPv6 packets + else if (eth_proto == bpf_htons(ETH_P_IPV6)) + { + ; + if (!(ipv6 = parse_ipv6hdr(&c)) || bpf_htons(ipv6->nexthdr != IPPROTO_UDP)) { + return XDP_PASS; + } + // if TC bit must not be set, apply the action + if ((r = udp_dns_reply_v6(&c, ipv6->saddr)) != TC) { + return r == DROP ? XDP_DROP : XDP_PASS; + } + + // swap src/dest IP addresses + struct in6_addr swap_ipv6 = ipv6->daddr; + ipv6->daddr = ipv6->saddr; + ipv6->saddr = swap_ipv6; + + } + // pass all non-IP packets + else { + return XDP_PASS; + } + } else { + return XDP_PASS; + } + + // swap MAC addresses + uint8_t swap_eth[ETH_ALEN]; + memcpy(swap_eth, eth->h_dest, ETH_ALEN); + memcpy(eth->h_dest, eth->h_source, ETH_ALEN); + memcpy(eth->h_source, swap_eth, ETH_ALEN); + + // bounce the request + return XDP_TX; +} diff --git a/contrib/xdp.py b/contrib/xdp.py new file mode 100644 index 0000000000..f2c4f1246d --- /dev/null +++ b/contrib/xdp.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +from bcc import BPF +import ctypes as ct +import netaddr +import socket + +# Constants +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 = {29: 'LOC', 255: '*', 251: 'IXFR', 100: 'UINFO', 50: 'NSEC3', 28: 'AAAA', 5: 'CNAME', 14: 'MINFO', 31: 'EID', 27: 'GPOS', 19: 'X25', 13: 'HINFO', 257: 'CAA', 10: 'NULL', 48: 'DNSKEY', 43: 'DS', 20: 'ISDN', 6: 'SOA', 17: 'RP', 101: 'UID', 58: 'TALINK', 249: 'TKEY', 26: 'PX', 23: 'NSAP-PTR', 16: 'TXT', 45: 'IPSECKEY', 39: 'DNAME', 254: 'MAILA', 18: 'AFSDB', 44: 'SSHFP', 2: 'NS', 12: 'PTR', 99: 'SPF', 32768: 'TA', 1: 'A', 30: 'NXT', 252: 'AXFR', 57: 'RKEY', 25: 'KEY', 32: 'NIMLOC', 38: 'A6', 52: 'TLSA', 8: 'MG', 55: 'HIP', 47: 'NSEC', 102: 'GID', 33: 'SRV', 32769: 'DLV', 51: 'NSEC3PARAM', 103: 'UNSPEC', 250: 'TSIG', 34: 'ATMA', 46: 'RRSIG', 41: 'OPT', 3: 'MD', 35: 'NAPTR', 4: 'MF', 7: 'MB', 49: 'DHCID', 15: 'MX', 253: 'MAILB', 37: 'CERT', 56: 'NINFO', 42: 'APL', 9: 'MR', 24: 'SIG', 11: 'WKS', 36: 'KX', 22: 'NSAP', 21: 'RT', 40: 'SINK'} +ACTIONS = {1 : 'DROP', 2 : 'TC'} + +DROP_ACTION = 1 +TC_ACTION = 2 + +# The interface on wich the filter will be attached +DEV = "eth0" + +# The list of blocked IPv4, IPv6 and QNames +# IP format : (IPAddress, Action) +# QName format : (QName, QType, Action) +blocked_ipv4 = [("192.0.2.1", TC_ACTION)] +blocked_ipv6 = [("2001:db8::1", TC_ACTION)] +blocked_qnames = [("localhost", "A", DROP_ACTION), ("test.com", "*", TC_ACTION)] + +# Main +xdp = BPF(src_file="xdp-filter.ebpf.src") + +fn = xdp.load_func("xdp_dns_filter", BPF.XDP) +xdp.attach_xdp(DEV, fn, 0) + +v4filter = xdp.get_table("v4filter") +v6filter = xdp.get_table("v6filter") +qnamefilter = xdp.get_table("qnamefilter") + +for ip in blocked_ipv4: + print(f"Blocking {ip}") + key = v4filter.Key(int(netaddr.IPAddress(ip[0]).value)) + leaf = v4filter.Leaf() + leaf.counter = 0 + leaf.action = ip[1] + v4filter[key] = leaf + +for ip in blocked_ipv6: + print(f"Blocking {ip}") + ipv6_int = int(netaddr.IPAddress(ip[0]).value) + ipv6_bytes = bytearray([(ipv6_int & (255 << 8*(15-i))) >> (8*(15-i)) for i in range(16)]) + key = (ct.c_uint8 * 16).from_buffer(ipv6_bytes) + leaf = v6filter.Leaf() + leaf.counter = 0 + leaf.action = ip[1] + v6filter[key] = leaf + +for qname in blocked_qnames: + print(f"Blocking {qname}") + key = qnamefilter.Key() + qn = bytearray() + for sub in qname[0].split('.'): + qn.append(len(sub)) + for ch in sub: + qn.append(ord(ch)) + qn.extend((0,) * (255 - len(qn))) + key.qname = (ct.c_ubyte * 255).from_buffer(qn) + key.qtype = ct.c_uint16(QTYPES[qname[1]]) + leaf = qnamefilter.Leaf() + leaf.counter = 0 + leaf.action = qname[2] + qnamefilter[key] = leaf + +print("Filter is ready") +try: + xdp.trace_print() +except KeyboardInterrupt: + pass + +for item in v4filter.items(): + print(f"{str(netaddr.IPAddress(item[0].value))} ({ACTIONS[item[1].action]}): {item[1].counter}") +for item in v6filter.items(): + print(f"{str(socket.inet_ntop(socket.AF_INET6, item[0]))} ({ACTIONS[item[1].action]}): {item[1].counter}") +for item in qnamefilter.items(): + print(f"{''.join(map(chr, item[0].qname)).strip()}/{INV_QTYPES[item[0].qtype]} ({ACTIONS[item[1].action]}): {item[1].counter}") + +xdp.remove_xdp(DEV, 0)