You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

小端机器下C语言Netfilter内核模块解析DNS包及崩溃排查

问题根源与修复方案

核心问题拆解

  1. QNAME字节序理解完全错误
    DNS报文采用网络字节序(大端),但QNAME的长度字段是单字节无符号整数,不存在字节序转换的需求——你错误地将整个QNAME序列按多字节值反转,导致把终止符0当成了第一个字节,直接触发后续逻辑错误,这就是内核日志显示"Blocked DNS request for 0"的原因。

  2. 内存越界与空指针解引用
    解析QNAME时未校验skb的剩余数据长度,读取超出报文范围的内存,触发非法内存访问;同时错误的字节序处理导致无法找到终止的0字节,大概率进入死循环,占用CPU不释放,触发看门狗的"soft lockup"警告。

  3. 模块卸载残留
    模块崩溃时,Netfilter钩子函数可能未被正确注销,内核认为模块仍在占用资源,导致重新加载时提示"Device or resource busy"。


修复步骤与代码示例

1. 正确解析QNAME

严格遵循RFC1035规范:QNAME由多个标签组成,每个标签前是单字节长度,以0字节结尾。解析时必须:

  • 直接读取单字节长度,无需字节序转换
  • 每次读取前校验skb剩余数据长度,防止越界
  • 设置最大迭代次数,避免畸形报文导致死循环
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/string.h>
#include <linux/module.h>

#define MAX_QNAME_LEN 256

// 解析DNS QNAME,返回下一个字段的偏移,失败返回-1
static int parse_dns_qname(struct sk_buff *skb, unsigned int offset, char *buf, size_t buf_size) {
    unsigned int pos = offset;
    size_t buf_pos = 0;
    u8 label_len;

    // 限制最大迭代次数,防止死循环
    while (pos < skb->len && buf_pos < buf_size - 1) {
        label_len = skb->data[pos++];
        if (label_len == 0) break; // QNAME终止符

        // 校验剩余数据是否足够读取当前标签
        if (pos + label_len > skb->len) {
            pr_err("DNS QNAME out of bounds\n");
            return -1;
        }

        // 添加域名分隔符(第一个标签前不加)
        if (buf_pos > 0) {
            buf[buf_pos++] = '.';
        }

        // 复制标签内容到缓冲区
        memcpy(buf + buf_pos, skb->data + pos, label_len);
        buf_pos += label_len;
        pos += label_len;
    }

    // 检查是否正常终止
    if (label_len != 0) {
        pr_err("DNS QNAME missing terminator\n");
        return -1;
    }

    buf[buf_pos] = '\0';
    return pos;
}

2. 安全的Netfilter钩子处理

  • 校验报文类型(UDP、DNS端口)
  • 确保skb可访问(skb_make_writable)
  • 严格校验偏移量,避免越界访问
static unsigned int dns_filter_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) {
    struct iphdr *iph;
    struct udphdr *udph;
    char qname[MAX_QNAME_LEN];
    int next_offset;
    unsigned int dns_hdr_end;

    // 只处理IPv4 UDP报文
    iph = ip_hdr(skb);
    if (iph->protocol != IPPROTO_UDP) {
        return NF_ACCEPT;
    }

    udph = udp_hdr(skb);
    // 处理DNS请求(目标端口53),根据需求可调整为响应(源端口53)
    if (ntohs(udph->dest) != 53) {
        return NF_ACCEPT;
    }

    // 确保skb数据可读写
    if (!skb_make_writable(skb, skb->len)) {
        pr_err("Failed to make skb writable\n");
        return NF_ACCEPT;
    }

    // DNS头部固定12字节,Question段从IP头+UDP头+12字节开始
    dns_hdr_end = sizeof(struct iphdr) + sizeof(struct udphdr) + 12;
    if (dns_hdr_end >= skb->len) {
        pr_debug("DNS packet too short\n");
        return NF_ACCEPT;
    }

    // 解析QNAME
    next_offset = parse_dns_qname(skb, dns_hdr_end, qname, sizeof(qname));
    if (next_offset < 0) {
        return NF_ACCEPT;
    }

    // 替换为你的过滤逻辑
    if (strcmp(qname, "cppreference.com") == 0) {
        pr_info("Blocked DNS request for %s\n", qname);
        return NF_DROP;
    }

    return NF_ACCEPT;
}

static struct nf_hook_ops dns_hook_ops = {
    .hook = dns_filter_hook,
    .pf = PF_INET,
    .hooknum = NF_INET_LOCAL_OUT, // 拦截本机发起的请求用这个钩子点
    .priority = NF_IP_PRI_FIRST,
};

3. 正确的模块初始化与清理

确保模块退出时注销钩子,避免残留:

static int __init dns_filter_init(void) {
    int ret;
    ret = nf_register_hook(&dns_hook_ops);
    if (ret != 0) {
        pr_err("Failed to register Netfilter hook: %d\n", ret);
        return ret;
    }
    pr_info("DNS filter module loaded\n");
    return 0;
}

static void __exit dns_filter_exit(void) {
    nf_unregister_hook(&dns_hook_ops);
    pr_info("DNS filter module unloaded\n");
}

module_init(dns_filter_init);
module_exit(dns_filter_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("DNS Request Filter Module");
MODULE_AUTHOR("Your Name");

额外注意事项

  • 多Question段处理:如果需要支持多个QNAME,需读取DNS头部的QDCOUNT字段(ntohs(*(u16*)(skb->data + dns_hdr_end - 2))),循环解析每个QNAME。
  • 模块崩溃后卸载:如果rmmod无法卸载,可尝试rmmod -f dns_filter_module(谨慎使用,可能导致内核不稳定),或直接重启系统。
  • 哈希冲突:如果需要拦截大量域名,建议使用内核哈希表(struct hlist_head)存储,避免多次字符串比较的性能开销。

内容的提问来源于stack exchange,提问作者Ostap

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 03:17:05