Netfilter钩子驱动实现轻量级报文指纹过滤,核心是注册钩子函数、提取端口/协议/载荷特征并快速比对,直接在内核协议栈判定丢弃或标记,不依赖iptables等用户态工具,延迟低、开销小。

在 Linux 内核中,利用 Netfilter 钩子编写轻量级报文指纹过滤驱动,核心是:注册钩子函数、提取关键字段(如 TCP/UDP 端口、协议、载荷特征等)、做快速比对、决定是否丢弃或标记。它不依赖用户态工具(如 iptables/nftables 规则),而是直接在内核协议栈关键路径上做判定,延迟低、开销小。
一、明确“指纹”定义和匹配粒度
“指纹”不是全包内容匹配,而是可稳定标识某类流量的轻量特征组合。常见选择包括:
- 五元组 + 协议标志:源/目的 IP + 源/目的端口 + 协议号(如 TCP SYN+ACK+PSH 组合)
- 固定载荷偏移特征:如 TLS ClientHello 固定位置的字节(0x16 0x03)、HTTP GET /admin 的前 12 字节(需确保 skb 数据可读)
- 长度+标志组合:如 UDP 包长 = 74 且目的端口 = 53(某些 DNS 探针)
避免使用动态字段(如时间戳、随机序列号)或需深度解析的字段(如 HTTP Host 头全名)。目标是单次 skb 访问、常数时间判断。
二、编写最小可行钩子模块(IPv4 + NF_INET_PRE_ROUTING)
以丢弃匹配特定 TCP 目的端口(如 65535)的报文为例,代码结构精简如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/tcp.h>
<p>static struct nf_hook_ops nfho = {
.hook = hook_func,
.pf = PF_INET,
.hooknum = NF_INET_PRE_ROUTING, // 早于路由,所有入向 IPv4 包
.priority = NF_IP_PRI_FIRST, // 最高优先级(可选)
};</p><p>unsigned int hook_func(void <em>priv, struct sk_buff </em>skb,
const struct nf_hook_state <em>state) {
struct iphdr </em>iph;
struct tcphdr *tcph;</p><pre class="brush:php;toolbar:false;">if (!skb || !(iph = ip_hdr(skb))) return NF_ACCEPT;
if (iph->protocol != IPPROTO_TCP) return NF_ACCEPT;
// 确保 TCP 头完整且可访问(避免分片或头被裁剪)
if (skb->len < iph->ihl*4 + sizeof(struct tcphdr)) return NF_ACCEPT;
tcph = (struct tcphdr *)((u8*)iph + iph->ihl*4);
if (ntohs(tcph->dest) == 65535) {
kfree_skb(skb); // 直接释放,不进入协议栈
return NF_STOLEN; // 表示已处理完毕
}
return NF_ACCEPT;}
static int __init init_module(void) { nf_register_net_hook(&init_net, &nfho); printk(KERN_INFO "fingerprint_filter: loaded\n"); return 0; }
static void __exit cleanup_module(void) { nf_unregister_net_hook(&init_net, &nfho); printk(KERN_INFO "fingerprint_filter: unloaded\n"); } MODULE_LICENSE("GPL");
注意点:
- 用
NF_STOLEN替代NF_DROP可省去 Netfilter 统计开销; - 必须检查
skb->len和 IP/TCP 头长度,防止越界访问导致 oops; - 若需读取载荷(如第 40 字节),用
skb_header_pointer()安全提取,不要直接指针解引用; - 避免在钩子中睡眠、分配大内存、调用可能阻塞的函数。
三、支持简单载荷指纹(如 TLS ClientHello)
若需匹配 TLS 握手起始字节(0x16 0x03),在确认是 TCP 且含数据后操作:
// 继续在 hook_func 中(TCP 分支后)
if (tcph->syn || tcph->fin || tcph->rst) return NF_ACCEPT; // 跳过控制报文
if (!tcph->doff || ntohs(tcph->doff) < 5) return NF_ACCEPT;
<p>int tcp_header_len = tcph->doff <em> 4;
int payload_len = ntohs(iph->tot_len) - iph->ihl</em>4 - tcp_header_len;
if (payload_len < 2) return NF_ACCEPT;</p><p>// 安全读取前两个字节
u8 tls_sig[2];
if (skb_header_pointer(skb,
(u8*)tcph - skb->data + tcp_header_len,
2, &tls_sig) != NULL) {
if (tls_sig[0] == 0x16 && (tls_sig[1] == 0x03 || tls_sig[1] == 0x01)) {
kfree_skb(skb);
return NF_STOLEN;
}
}
该方式不拷贝整个载荷,仅按需提取固定偏移字节,保持轻量。
四、编译、加载与验证
Makefile 示例:
obj-m += fingerprint_filter.o KDIR := /lib/modules/$(shell uname -r)/build <p>all: make -C $(KDIR) M=$(PWD) modules</p><p>clean: make -C $(KDIR) M=$(PWD) clean
加载后验证:
sudo insmod fingerprint_filter.ko- 用
tcpdump -i any port 65535发包测试是否消失 -
dmesg | tail查看加载/卸载日志 - 必要时加
printk打点(但上线前应移除,避免性能影响)
不复杂但容易忽略:务必在不同网络场景(如分片包、TSO/GSO 启用网卡)下测试,必要时在 NF_INET_LOCAL_IN 或启用 nf_ct_untrack 避免连接跟踪干扰。



















