这篇解决什么问题
DNS查询监控的生产环境实践和踩坑记录。
先说结论
DNS监控的关键是正确解析DNS协议、处理UDP/TCP双协议、合理设计map结构。
现场背景
2024年9月,我们需要监控DNS查询行为。
需求:
- 记录所有DNS查询
- 统计查询频率
- 检测异常查询
- 分析DNS延迟
我最开始的思路
方案选择
考虑的方案:
- tcpdump
- dnstrace
- eBPF实现
为什么不用tcpdump
tcpdump的优势:
- 功能强大
- 易于使用
但问题:
- 性能开销大
- 无法关联到进程
- 难以做实时分析
为什么不用dnstrace
dnstrace的优势:
- 专门监控DNS
- 功能丰富
但问题:
- 需要修改DNS配置
- 性能开销中等
- 无法做复杂分析
真正的技术路径
DNS协议分析
DNS协议特点:
- 默认使用UDP 53端口
- 大响应使用TCP 53端口
- 查询和响应格式不同
DNS查询格式:
+---------------------+
| Header (12 bytes) |
+---------------------+
| Question (variable) |
+---------------------+
DNS响应格式:
+---------------------+
| Header (12 bytes) |
+---------------------+
| Question (variable) |
+---------------------+
| Answer (variable) |
+---------------------+
| Authority (variable)|
+---------------------+
| Additional (variable)|
+---------------------+
eBPF实现
设计数据结构:
c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>
struct dns_event {
__u32 pid;
char comm[16];
__u32 src_ip;
__u32 dst_ip;
__u16 src_port;
__u16 dst_port;
char query[256];
__u64 timestamp;
__u64 latency;
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 100000);
__type(key, __u32);
__type(value, __u64);
} query_map SEC(".maps");
解析DNS查询:
c
SEC("xdp")
int xdp_dns(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) {
return XDP_PASS;
}
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) {
return XDP_PASS;
}
if (ip->protocol != IPPROTO_UDP && ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
__u16 src_port, dst_port;
if (ip->protocol == IPPROTO_UDP) {
struct udphdr *udp = (void *)(ip + 1);
if ((void *)(udp + 1) > data_end) {
return XDP_PASS;
}
src_port = udp->source;
dst_port = udp->dest;
if (dst_port != bpf_htons(53)) {
return XDP_PASS;
}
void *dns_data = (void *)(udp + 1);
if (dns_data + 12 > data_end) {
return XDP_PASS;
}
__u16 *dns_id = (__u16 *)dns_data;
__u64 *query_time = bpf_map_lookup_elem(&query_map, dns_id);
if (!query_time) {
__u64 now = bpf_ktime_get_ns();
bpf_map_update_elem(&query_map, dns_id, &now, BPF_ANY);
} else {
struct dns_event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (e) {
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(e->comm, sizeof(e->comm));
e->src_ip = ip->saddr;
e->dst_ip = ip->daddr;
e->src_port = src_port;
e->dst_port = dst_port;
bpf_probe_read_user_str(e->query, sizeof(e->query),
dns_data + 12);
e->timestamp = bpf_ktime_get_ns();
e->latency = e->timestamp - *query_time;
bpf_ringbuf_submit(e, 0);
}
bpf_map_delete_elem(&query_map, dns_id);
}
}
return XDP_PASS;
}
验证过程
测试DNS查询
bash
# 查询DNS
dig www.example.com
# 查看事件
sudo bpftool map dump id 2
输出:
pid: 1234, comm: dig, src_ip: 192.168.1.100, dst_ip: 8.8.8.8
query: www.example.com, latency: 5000
测试性能
bash
# 压力测试
for i in {1..1000}; do
dig www.example.com > /dev/null
done
# 查看统计
sudo bpftool prog show
性能:
- CPU占用:2%
- 延迟:5us
- 吞吐量:10K queries/s
为什么不用其他方案
为什么不用tcpdump
tcpdump的优势:
- 功能强大
- 易于使用
但问题:
- 性能开销大
- 无法关联到进程
- 难以做实时分析
为什么不用dnstrace
dnstrace的优势:
- 专门监控DNS
- 功能丰富
但问题:
- 需要修改DNS配置
- 性能开销中等
- 无法做复杂分析
eBPF的优势
- 性能好(2% CPU)
- 可以关联到进程
- 可以做复杂分析
- 可以处理UDP/TCP双协议
线上注意事项
1. 处理UDP/TCP双协议
DNS同时使用UDP和TCP:
c
// 错误:只处理UDP
if (ip->protocol != IPPROTO_UDP) {
return XDP_PASS;
}
// 正确:处理UDP和TCP
if (ip->protocol != IPPROTO_UDP && ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
2. 解析DNS协议
DNS协议解析要正确:
c
// DNS header
struct dns_header {
__u16 id;
__u16 flags;
__u16 qdcount;
__u16 ancount;
__u16 nscount;
__u16 arcount;
};
3. 控制map大小
map大小要合理:
c
// 错误:太大,浪费内存
__uint(max_entries, 1000000);
// 正确:根据实际需求设置
__uint(max_entries, 100000);
4. 定期清理map
map会一直增长,需要定期清理:
c
void cleanup_query_map(int map_fd)
{
__u32 key, next_key;
__u64 value;
__u64 current_time = get_current_time_ns();
while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
bpf_map_lookup_elem(map_fd, &next_key, &value);
if (current_time - value > TIMEOUT) {
bpf_map_delete_elem(map_fd, &next_key);
}
key = next_key;
}
}
小结
DNS监控的关键是正确解析DNS协议。
eBPF实现DNS监控的优势:
- 性能好:2% CPU
- 实时性:5us延迟
- 关联进程:可以记录进程信息
- 双协议:支持UDP和TCP
实现要点:
- 正确解析DNS协议
- 处理UDP/TCP双协议
- 使用map匹配查询和响应
- 定期清理map
工程实践的关键是:理解DNS协议,正确解析,处理双协议。