]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add a sample XDP program and associated python script in contrib
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 29 Oct 2021 14:56:08 +0000 (16:56 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 16 Nov 2021 08:02:48 +0000 (09:02 +0100)
Both contributed by Pierre Grié <pierre.grie@nameshield.net>.

contrib/xdp-filter.ebpf.src [new file with mode: 0644]
contrib/xdp.py [new file with mode: 0644]

diff --git a/contrib/xdp-filter.ebpf.src b/contrib/xdp-filter.ebpf.src
new file mode 100644 (file)
index 0000000..46e3123
--- /dev/null
@@ -0,0 +1,430 @@
+#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, &eth_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 (file)
index 0000000..f2c4f12
--- /dev/null
@@ -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)