Securing BIND 9 with eBPF/XDP Workshop

1 BIND 9 Security workshop

The virtual machines have a domain name in the form

Please login to the machines with a modern web browser under the URL with the username user and the password DNSandBIND. You can also login with SSH and the same username and password.

The virtual machines run the cockpit tool ( to provide a terminal in the web browser.

Then select the terminal (last menu option on the left) and start the tutorial.

1.1 Install BIND 9 server

  • As the user root, install the BIND 9 DNS Server
% apt install bind9
  • Replace the BIND 9 configuration in /etc/bind/named with the simple configuration below
options {
  directory "/var/cache/bind";
  allow-recursion { localhost };
  • Check the configuration for errors and reload the BIND 9 server
% named-checkconf
% rndc reconfig
  • Find the (public) IPv4 (or IPv6) address of your server
% hostname -I
; <<>> DiG 9.16.22 <<>> @ ch txt version.bind
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14922
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

; EDNS: version: 0, flags:; udp: 1232
; COOKIE: 86bfaf26127e2c3c01000000619356412aabbab08bd3eabf (good)
;version.bind.			CH	TXT

version.bind.		0	CH	TXT	"9.16.22-Debian"

;; Query time: 39 msec
;; WHEN: Tue Nov 16 00:57:05 CST 2021
;; MSG SIZE  rcvd: 96
  • Great, your BIND 9 server is reachable over the Internet

2 Session 1 - Blocking all UDP traffic except DNS (with XDP/eBPF)

  • Install the BCC Tools and the Linux header for the current kernel
% apt install bpfcc-introspection  bpfcc-tools python3-bpfcc
% apt install linux-headers-$(uname -r)
  • Save the following C source code into the file drop-non-dns-udp.c., This eBPF program will
    • be executed on every network packet
    • print the text got a packet for every network packet received
    • extract the IP and UDP header from the packet
    • if neither the UDP source nor the UDP destination port is 53 (DNS), the return code XDP_DROP is returned, dropping the packet. A message is reported when dropping the packet
    • all other packets are given to the Linux TCP/IP stack with the return code XDP_PASS
    • production XDP code should not use bpf_trace_printk to communicate information, but should write the data into a map structure that will then be read from the user-space loader
#define KBUILD_MODNAME "filter"
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/udp.h>

int udpfilter(struct xdp_md *ctx) {
  bpf_trace_printk("got a packet\n");
  void *data = (void *)(long)ctx->data;
  void *data_end = (void *)(long)ctx->data_end;
  struct ethhdr *eth = data;
  if ((void*)eth + sizeof(*eth) <= data_end) {
    struct iphdr *ip = data + sizeof(*eth);
    if ((void*)ip + sizeof(*ip) <= data_end) {
      if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void*)ip + sizeof(*ip);
        if ((void*)udp + sizeof(*udp) <= data_end) {
          if ((udp->dest != ntohs(53)) && (udp->source != ntohs(53))) {
            if (udp->dest != udp->source) {
              bpf_trace_printk("drop udp src/dest port %d/%d\n", ntohs(udp->source), ntohs(udp->dest));
              return XDP_DROP;
  return XDP_PASS;
  • Save the eBPF loader (written in Python) into the file
    • This loader will compile the eBPF program above, load it into the Linux kernel and will attach it to the loopback network interface (lo)
    • There will be warnings during compilation that can be ignored (for this test)
    • The virtio driver used in the lab machines do not allow XDP programs on the eth0 and eth1 interfaces, so we're testing with the loopback lo interface
#!/usr/bin/env python3

from bcc import BPF
import time

device = "lo"
b = BPF(src_file="drop-non-dns-udp.c")
fn = b.load_func("udpfilter", BPF.XDP)
b.attach_xdp(device, fn, 0)

except KeyboardInterrupt:

b.remove_xdp(device, 0)
  • Make the python program executable
% chmod +x
  • Execute the Python loader program (there will be warnings that we can ignore)
% ./
  • Login to the lab machine with a new connection (or use tmux)
  • Execute DNS queries towards the BIND 9 DNS server via the loopback interface. This query should not trigger the XDP program to drop packets
% dig @localhost
  • If we send the DNS query to a port other than port 53, the packet get's dropped
% dig -p 5353 @localhost

3 Session 2 - Instrumenting BIND 9

  • Configure BIND 9 to forward all request for the domain to Quad9 ( In the file /etc/bind/named.conf add
zone "" {
  type forward;
  forwarders {; };
  • Check the configuration with named-checkconf and reload the BIND 9 DNS server with rndc reconfig
  • Install bpftrace
apt install bpftrace
  • Save the following source into the file

struct dns_name {
        unsigned int   magic;
        unsigned char *ndata;
        unsigned int   length;
        unsigned int   labels;
        unsigned int   attributes;
        unsigned char *offsets;
//      isc_buffer_t  *buffer;
//      ISC_LINK(dns_name_t) link;
//      ISC_LIST(dns_rdataset_t) list;

  print("Waiting for forward decision...\n");
  @dns_name[tid] = ((struct dns_name *)arg1)->ndata

 if (retval == 0) {
    printf("Forwarded domain name: %s\n", str(@dns_name[tid]));
  • Make the file executeable
% chmod +x
  • Execute the bpftrace script
% ./
  • Login to the lab machine from a different terminal (or use tmux). Flush the cache of the BIND 9 resolver with rndc flush, then execute a DNS name resolution for and for other domains. See that the bpftrace script reports the forward decisions in BIND 9
% dig @localhost

4 Session 3 - bpttrace tools and on-liners

  • Switch the operating system to use the BIND 9 DNS resolver
% echo "nameserver" > /etc/resolv.conf
  • Start in one terminal, execute some other network tools that require DNS name resolution (ping, apt, traceroute etc) to see the DNS name resolution latency
  • Count UDP send/receives by on-CPU PID and process name (Excerpt From "BPF Performance Tools" by Brendan Gregg)
    • Execute some DNS queries against the BIND 9 resolver
    • Exec the bpftrace script with CTRL+C to see the report
% bpftrace -e "k:udp*_sendmsg,k:udp*_recvmsg { @[func, pid, comm] = count(); }"
  • Show packets received over UDP by the named process as a histogram (execute some queries, then terminate the script with CTRL+C)
% bpftrace -e "kr:udp_recvmsg /pid == $(pgrep named)/ { @recv_bytes = hist(retval); }"
  • Histogram of UDP packets send by the BIND 9 process (execute some queries, then terminate the script with CTRL+C)
% bpftrace -e "kprobe:udp_sendmsg /pid == $(pgrep named)/ { @size = hist(arg2); }"
  • Example of BIND 9 tracing: print whenever rndc status is executed
% bpftrace -e 'uprobe:named:named_server_status { print("rndc status executed") }'
  • How long does it take (in nanoseconds) to flush the cache with rndc flush? Do some queries against the BIND 9 DNS resolver to fill the cache. Then call rndc flush to flush the cache. The printed value should get higher the more cache entries need to be cleaned.
% bpftrace -e "uprobe:/usr/sbin/named:named_server_flushcache / pid == $(pgrep named) /{ @start[tid] = nsecs; }
    uretprobe:/usr/sbin/named:named_server_flushcache /@start[tid]/ { print(nsecs - @start[tid]); delete(@start[tid]); }"
  • Trace the cache related functions BIND 9 executes. Send some queries to the BIND 9 DNS resolver, then use rndc flush, rndc flushname <domain-name> and rndc flushtree <name> and see which functions are executed inside BIND 9
% bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/* { print(func) }'