DNS监控:生产实践

这篇解决什么问题

DNS查询监控的生产环境实践和踩坑记录。

先说结论

DNS监控的关键是正确解析DNS协议、处理UDP/TCP双协议、合理设计map结构。

现场背景

2024年9月,我们需要监控DNS查询行为。

需求:

  1. 记录所有DNS查询
  2. 统计查询频率
  3. 检测异常查询
  4. 分析DNS延迟

我最开始的思路

方案选择

考虑的方案:

  1. tcpdump
  2. dnstrace
  3. 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监控的优势:

  1. 性能好:2% CPU
  2. 实时性:5us延迟
  3. 关联进程:可以记录进程信息
  4. 双协议:支持UDP和TCP

实现要点:

  1. 正确解析DNS协议
  2. 处理UDP/TCP双协议
  3. 使用map匹配查询和响应
  4. 定期清理map

工程实践的关键是:理解DNS协议,正确解析,处理双协议

相关推荐
罗技1232 小时前
记一次硬盘满了导致 Coco Server 无法启动的排查
网络·网络协议·rpc
Unpredictable2222 小时前
Ubuntu 22.04 ROS2 Humble 源码编译安装 teb_local_planner 踩坑记录
linux·ubuntu·teb·自主导航·局部路径规划
小王C语言2 小时前
【进程概念】————进程切换、环境变量
linux·运维·服务器
饮长安千年月2 小时前
Linux下的敏感目录
linux·网络·数据库·web安全
NEXT062 小时前
从输入 URL 到页面展示的完整链路解析
网络协议·面试·浏览器
Codefengfeng3 小时前
压缩包的恢复
linux·运维
楼田莉子3 小时前
Linux网络学习:网络的基础概念
linux·运维·服务器·网络·c++·学习
Starry_hello world4 小时前
Linux 网络 (4)
linux·网络
苏叶新城4 小时前
Glibc的版本在centos 7到9对应关系
linux·运维·centos