你是否想过,如何绕过传统监控方案的局限,在内核层面对DNS流量进行细粒度追踪?面对DNS缓存投毒、隧道窃密等日益复杂的威胁,仅靠应用层日志往往力不从心。本文将带你深入实践,利用eBPF技术直接在内核中捕获并解析DNS数据包,构建一个实时DNS查询与响应追踪工具,为你的网络安全监控体系提供全新视角。
本文译自 Tracing DNS Queries in Real Time with eBPF。
许多依赖自建DNS服务器的组织,都面临着防范以DNS为中心的各种威胁的责任,例如缓存投毒、放大攻击或通过DNS隧道进行的隐蔽数据窃取。
日志记录是发现这些攻击的关键,但大多数DNS守护进程默认并不会记录响应。虽然你可以使用dnstap这类方案,但它通常要求你重新编译DNS服务器软件才能生效。
于是,我们提出了一个更直接的想法:为什么不利用eBPF直接在内核层面捕获DNS流量呢?这样做无需对现有服务做任何修改。
🚀 特别感谢Fortinet的系统安全工程师Geoffrey Bucchino撰写了这篇深入且实用的客座文章!
近年来,随着全球紧张局势加剧,网络安全已成为每个组织的首要任务。国家和网络犯罪团伙都在大力投资攻击能力,不断探测网络中的薄弱环节。
仅在2024年第一季度,攻击者就发起了超过 150万次针对DNS层的DDoS攻击,这足以凸显针对DNS基础设施的威胁规模。现代DNS威胁还包括勒索——2024年第二季度,12.3%的客户报告称曾因DDoS攻击受到威胁或勒索,攻击者利用服务中断来索取赎金。
这一切使得主动的DNS监控变得至关重要,我们需要能够记录每一次响应,并尽早识别异常。
考虑到攻击流量巨大,如果监控方案本身会给系统带来显著负载,那显然不是理想的选择。为此,我们开发了基于eBPF的DNS-Trace工具,用于跟踪和记录DNS查询及响应的具体内容。

其设计思路非常直接:
- 附加一个eBPF程序到原始套接字,用以捕获流经指定网络接口的所有数据包。
- 分析每个数据包,判断其是否为DNS查询或响应,然后解析并记录关键信息,供用户空间程序读取。
💡 这是一个被动监控工具——它本身不会阻止或预防攻击。但它展示了一个非常有价值的概念:如何在内核中,使用eBPF直接处理DNS查询和响应数据包。
完整的代码可以在 Gitea仓库 中找到。下面的部分,我们将逐步拆解其实现的技术细节。
代码讲解与实现
在我们的用户空间应用程序中,我们使用libbpf库来加载和管理eBPF程序,并将其附加到我们感兴趣的原始套接字上。
sock = create_rsock(arguments.interface);
if (sock == -1){
printf("Failed to listen to the interface %s\n", arguments.interface);
exit(-1);
}
// ...
// Retrieving the eBPF program
programSkb = bpf_object__find_program_by_name(obj, "detect_dns");
if (!programSkb){
printf("Failed to find program\n");
bpf_object__close(obj);
return -1;
}
// Attach eBPF program to the socket
bpf_program__attach(programSkb);
int prog_fd = bpf_program__fd(programSkb);
setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(int));
而在我们的eBPF程序中,我们从作为程序输入的 struct __sk_buff 变量中捕获并读取数据包。
SEC("socket")
int detect_dns(struct __sk_buff *skb) {
// ...
}
__sk_buff 这个结构体包含了网络数据包的所有相关信息,例如各层头部(以太网、IP/IPv6、TCP/UDP)和有效载荷。
在这个概念验证中,我们简单地根据目标或源端口号(例如53)来判断一个数据包是DNS查询还是响应。更严谨的做法是解码DNS头部,并检查QR位是否设置为0(查询)或1(响应)。具体可参考 RFC 1035, 第4.1.1节。
DNS查询解析
通过检查DNS消息的Question部分(使用 dnsquery 函数),我们可以解析并收集查询的详细信息,例如记录类型(A、CNAME、MX、TXT等)、查询类(IN、CH、HS等)以及查询的主机名。更多细节参见 RFC 1035, 第4.1.2节。
解析查询类型和类相对简单,但要从DNS查询中提取出主机名,我们需要理解DNS的标签序列格式。
例如,如果有效载荷中域名 www.bucchino.org 的十六进制表示如下:

其解析规则如下:
- 第一个字节
0x03 表示第一个标签的长度,因此后面三个字节 0x77 对应标签 www。
- 第四个字节
0x08 表示下一个标签长度为8,后面八个字节对应标签 bucchino。
- 第十四个字节
0x03 表示第三个标签长度为3,后面三个字节对应标签 org。
- 最后,空字符
0x00 标志域名结束。
这种从字节流中提取标签序列的逻辑,是通过 get_labels 函数实现的。解析出的所有信息会被存入一个 event 结构体,并通过eBPF的环形缓冲区(ring buffer)发送到用户空间,由我们的应用程序读取并打印。
DNS响应解析
在DNS消息中,Question部分之后就是Answer部分(同样定义于RFC 1035)。我们使用 dnsanswer 函数,逐字节解码响应中的资源记录(Resource Record),并将其同样发送至用户空间。
将eBPF程序编译、加载并附加到网络接口后,剩下的就是见证“魔法”的时刻了。

未来改进方向
当前的DNS-Trace程序作为一个概念验证已经足够,但仍有不少可以增强的方向,例如:
- 集成攻击检测逻辑,用于识别潜在的DNS隧道活动。
- 完善对IPv6 DNS流量的全面支持。
- 增加对DNSSEC请求和响应的解析能力。
希望这份教程和代码能为你利用eBPF进行网络安全监控提供启发。社区是技术演进的重要动力,如果你有更好的想法或改进,欢迎在云栈社区的相关板块进行分享与探讨。