--- /dev/null
+#include <net/sock.h>
+#include <uapi/linux/udp.h>
+#include <uapi/linux/ip.h>
+#include <uapi/linux/ipv6.h>
+
+#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;
+}
--- /dev/null
+#!/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)