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协议,正确解析,处理双协议

相关推荐
sunxunyong1 天前
CGroup配置
linux·运维·服务器
hy____1231 天前
Linux_网络编程套接字
linux·运维·网络
IP搭子来一个1 天前
爬虫IP地址受限怎么办?附解决方法
网络·爬虫·tcp/ip
若风的雨1 天前
【deepseek】 Linux 调度延时分析
linux
北京耐用通信1 天前
协议融合的工业钥匙:耐达讯自动化网关如何打通CC-Link IE转DeviceNet的通信壁垒
人工智能·物联网·网络协议·自动化·信息与通信
EasyGBS1 天前
GB35114+GB28181:EasyGBS视频融合平台如何构建视频监控 “联网+安全” 双重保障体系
网络·人工智能·国标gb28181·gb35114
2301_803554521 天前
linux 以及 c++编程里对于进程,线程的操作
linux·运维·c++
LuDvei1 天前
windows 中 vs code远程连接linux
linux·运维·服务器·windows
GDAL1 天前
MANIFEST.in简介
linux·服务器·前端·python
点点滴滴的记录1 天前
Redis部署在Linux上性能高于Windows
linux·数据库·redis