Securing BIND 9 with eBPF/XDP Workshop
1 BIND 9 Security workshop
The virtual machines have a domain name in the form
ebpfNNN.dnslab.org.
Please login to the machines with a modern web browser under the URL
https://ebpfNNN.dnslab.org:9090 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
(https://cockpit-project.org) 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/namedwith 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
- Go to https://networking.ringofsaturn.com/Tools/dig.php (a web
based front-end to
dig) and query for@<ip-address> ch txt version.bind
; <<>> DiG 9.16.22 <<>> @138.197.66.242 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 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ; COOKIE: 86bfaf26127e2c3c01000000619356412aabbab08bd3eabf (good) ;; QUESTION SECTION: ;version.bind. CH TXT ;; ANSWER SECTION: version.bind. 0 CH TXT "9.16.22-Debian" ;; Query time: 39 msec ;; SERVER: 138.197.66.242#53(138.197.66.242) ;; 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_DROPis 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_printkto 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
drop-non-dns-udp.py.- 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
virtiodriver used in the lab machines do not allow XDP programs on theeth0andeth1interfaces, so we're testing with the loopbacklointerface
- This loader will compile the eBPF program above, load it into the
Linux kernel and will attach it to the loopback network 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)
try:
b.trace_print()
except KeyboardInterrupt:
pass
b.remove_xdp(device, 0)
- Make the python program executable
% chmod +x drop-non-dns-udp.py
- Execute the Python loader program (there will be warnings that we can ignore)
% ./drop-non-dns-udp.py
- 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 isc.org
- If we send the DNS query to a port other than port 53, the packet get's dropped
% dig -p 5353 @localhost isc.org
3 Session 2 - Instrumenting BIND 9
- Configure BIND 9 to forward all request for the domain
isc.orgto Quad9 (9.9.9.9). In the file/etc/bind/named.confadd
zone "isc.org" {
type forward;
forwarders { 9.9.9.9; };
};
- Check the configuration with
named-checkconfand reload the BIND 9 DNS server withrndc reconfig - Install
bpftrace
apt install bpftrace
- Save the following source into the file
forward-trace.bt
#!/usr/bin/bpftrace
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;
};
BEGIN
{
print("Waiting for forward decision...\n");
}
uprobe:/lib/x86_64-linux-gnu/libdns-9.16.22-Debian.so:dns_fwdtable_find
{
@dns_name[tid] = ((struct dns_name *)arg1)->ndata
}
uretprobe:/lib/x86_64-linux-gnu/libdns-9.16.22-Debian.so:dns_fwdtable_find
{
if (retval == 0) {
printf("Forwarded domain name: %s\n", str(@dns_name[tid]));
}
delete(@dns_name[tid]);
}
- Make the file executeable
% chmod +x forward-trace.bt
- Execute the
bpftracescript
% ./forward-trace.bt
- Login to the lab machine from a different terminal (or use
tmux). Flush the cache of the BIND 9 resolver withrndc flush, then execute a DNS name resolution forisc.organd for other domains. See that thebpftracescript reports the forward decisions in BIND 9
% dig @localhost isc.org
4 Session 3 - bpttrace tools and on-liners
- Switch the operating system to use the BIND 9 DNS resolver
% echo "nameserver 127.0.0.1" > /etc/resolv.conf
- Start
gethostlatency.btin one terminal, execute some other network tools that require DNS name resolution (ping,apt,tracerouteetc) 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
namedprocess 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 statusis 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 callrndc flushto 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>andrndc flushtree <name>and see which functions are executed inside BIND 9
% bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libdns-9.16.22-Debian.so:dns_cache* { print(func) }'