小端机器下C语言Netfilter内核模块解析DNS包及崩溃排查
问题根源与修复方案
核心问题拆解
QNAME字节序理解完全错误
DNS报文采用网络字节序(大端),但QNAME的长度字段是单字节无符号整数,不存在字节序转换的需求——你错误地将整个QNAME序列按多字节值反转,导致把终止符0当成了第一个字节,直接触发后续逻辑错误,这就是内核日志显示"Blocked DNS request for 0"的原因。内存越界与空指针解引用
解析QNAME时未校验skb的剩余数据长度,读取超出报文范围的内存,触发非法内存访问;同时错误的字节序处理导致无法找到终止的0字节,大概率进入死循环,占用CPU不释放,触发看门狗的"soft lockup"警告。模块卸载残留
模块崩溃时,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
相关产品推荐
相关产品推荐

