Many organizations rely on in-house DNS servers—and with that comes the responsibility to guard against DNS-centric threats like cache poisoning, amplification attacks, or covert data exfiltration via DNS tunneling.
Logging is essential to spot these attacks, but most DNS daemons don’t record responses by default. You can use dnstap, but it only works if you recompile your DNS software.
So we asked ourselves: why not just use eBPF in the kernel to tap into DNS traffic directly—no recompilation required.
🚀 Special thanks to Geoffrey Bucchino, System Security Engineer at Fortinet, for putting together this deep and practical guest post!
With tensions rising around the world in recent years, cybersecurity has moved to the top of every organization’s priority list.
Nation-states and cybercriminal groups alike are investing heavily in offensive capabilities, probing networks for weaknesses and exploiting any gap they find.
In the first quarter of 2024 alone, attackers launched over 1.5 million DNS-layer DDoS assaults, underscoring the sheer volume of threats targeting DNS infrastructure.
Modern DNS threats also include extortion — In Q2 2024, 12.3 percent of customers reported being threatened or extorted via DDoS, up from 10.2 percent the previous quarter, as attackers leverage service disruptions for ransom.
That makes active DNS monitoring essential—logging every response, rate-limiting clients, and using DDoS protection to catch attacks early.
Not only are these high-volume attacks, but it also wouldn’t be ideal if a monitoring solution imposed significant load on the system.
With this in mind, we developed DNS-Trace, based on eBPF, to track and log the content of both DNS queries and DNS responses.
The design is pretty straightforward:
Attach an eBPF program to the socket to capture network packets.
Analyze each packet to check whether it’s a DNS query or response, then process and log the result for the user.
💡 This is a passive tool—it doesn’t block or prevent attacks. Instead, it demonstrates a useful concept: how DNS queries and responses can be processed directly in the kernel using eBPF.
The full code is available in the following Gitea repository.
The sections below walk through the technical details of the implementation.
Code Walkthrough
In our user-space application, we use libbpf library and attach our eBPF program to a socket of our interest.
Using our eBPF program, we capture and read the packet from the variable struct __sk_buff, captured as an input of our eBPF program.
__sk_buff
structure contains all the information related to a network packet, such as the headers (Ethernet, IP/IPv6, TCP/UDP) and the payload.
Since this is a proof of concept, we determine whether the packet is a DNS query or response based on the destination/source port.
We could technically also decode the DNS header and inspect, whether QR bit is set to 0 (query) or 1 (response). See RFC 1035, section 4.1.1.
DNS Query
By inspecting the Question section of the DNS message using the dnsquery function, we can parse and gather the details about the query—such as the type (A, CNAME, MX, TXT, etc.), class (IN, CH, HS, etc.), and host. See RFC 1035, section 4.1.2 for more details.
While parsing the query type and class is relatively trivial, to resolve the host from the DNS query, we need to “make sense“ out of the sequence of labels.
For instance, if the domain name www.bucchino.org in the payload is set as:
This is in the hexadecimal format, where:
the first byte,
0x03
, represents the length of the first label, so it is followed by three bytes0x77
(www
).the fourth byte indicates a length of
0x08
, followed by the namebucchino
(0x62 0x75 0x63 0x63 0x68 0x69 0x6e 0x6f
).the 14th byte indicates a length of
0x03
, followed by theorg
label (0x6f 0x72 0x67
)And lastly, the null character
0x00
marks the end of the name.
Parsing of the labels (or I should say the domain name) is achieved through the get_labels function.
Once all this information is parsed, it is stored in the event
structure and pushed to the ring buffer, from where the user space application can read it.
DNS Response
Following the query section is the answer section (as described in RFC 1035, section 4.1.2).
Using the dnsanswer function, we decode it byte-by-byte and send it to user space.
Combining all of this, we run and attach our eBPF program to the network interface and the rest is the magic that you now understand.
DNS-Trace program could be improved in several ways, including:
Detect potential attacks, including DNS tunneling
IPv6 support
Analysis of DNSSEC requests
⏪ Did you miss the previous issues? I'm sure you wouldn't, but JUST in case:
I hope you find this resource helpful. Keep an eye out for more updates and developments in eBPF in next week's newsletter.
Until then, keep 🐝-ing!
Warm regards, Teodor and Geoffrey