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

相关推荐
万联WANFLOW2 分钟前
异地厂区视频监控远程运维:从组网到安全的实施要点
运维·网络·安全·企业组网·sdwan
衍玊43 分钟前
深入剖析 TCP/IP 网络通信原理与内核实现
服务器·网络·tcp/ip
.Bryce.1 小时前
从零到一开发技术名词大全:从需求分析到 AI Agent 工程
java·网络·人工智能·agent·需求分析·agent评测
FPGA小迷弟2 小时前
FPGA 与千兆以太网:各类物理接口的形式、区别、使用方法与应用场景详解
tcp/ip·macos·fpga开发·udp·fpga
Black蜡笔小新3 小时前
上云网关/端口穿透/设备上云EasyNTS:无需公网IP、也不改防火墙,设备怎么从内网跑到外网去
网络·网络协议·tcp/ip
女神下凡3 小时前
芯参谋(85):Linux_PPI_NAND_驱动开发指南
linux·运维·arm开发·驱动开发·单片机·嵌入式硬件·设计规范
科技小登3 小时前
出海高防CDN怎么选?CDN加速、边缘清洗与跨境回源POC指南
tcp/ip·web安全·ddos
12.=0.4 小时前
【持续更新】shell脚本 + Linux命令
linux·运维·服务器
Escalating_xu4 小时前
【Linux 网络编程】五种 I/O 模型与非阻塞 I/O:从“等数据”到正确处理 EAGAIN
java·linux·网络
无言听风雨丶4 小时前
Linux 容器本质探秘:Cgroup 与 Namespace 深度解析与实战
linux·运维·服务器·kubernetes