4. 钩子函数注册与注销:nf_register_net_hook 与 nf_unregister_net_hook API 详解、钩子优先级
Netfilter 钩子函数的生命周期由注册与注销两个核心操作管理。内核提供了 nf_register_net_hook 和 nf_unregister_net_hook 两个 API,用于将自定义的钩子函数挂载到指定的网络命名空间和钩子点上。本章将深入解析这两个 API 的原型、参数、返回值、使用模式,并重点阐述钩子优先级对数据包处理顺序的影响。
4.1 nf_register_net_hook API 详解
nf_register_net_hook 是 Linux 内核 4.13 版本之后推荐的注册接口,它显式要求传入网络命名空间指针,避免了全局命名空间操作的歧义。该函数将 nf_hook_ops 结构体实例插入到对应钩子点的优先级链表中。
4.1.1 函数原型与参数
#include <linux/netfilter.h>
int nf_register_net_hook(struct net *net,
const struct nf_hook_ops *reg);
| 参数 | 类型 | 描述 |
|---|---|---|
net |
struct net * |
目标网络命名空间指针。传入 &init_net 表示注册到初始命名空间;传入 net 表示注册到指定容器命名空间。 |
reg |
const struct nf_hook_ops * |
指向已填充的钩子操作结构体的指针。该结构体包含钩子函数指针、钩子点、协议族、优先级等关键字段。 |
4.1.2 返回值
| 返回值 | 含义 |
|---|---|
0 |
注册成功。钩子函数已加入处理链。 |
-EEXIST |
重复注册。相同优先级、相同钩子点的钩子已存在。 |
-ENOMEM |
内存不足,无法分配内部链表节点。 |
-EINVAL |
参数无效,例如 reg 为 NULL 或钩子点超出范围。 |
4.1.3 典型注册流程
- 定义
nf_hook_ops结构体实例,通常使用静态全局变量。 - 填充
hook字段为自定义钩子函数指针。 - 设置
pf字段为协议族,如NFPROTO_IPV4。 - 设置
hooknum字段为目标钩子点,如NF_INET_PRE_ROUTING。 - 设置
priority字段,决定在钩子链中的位置。 - 调用
nf_register_net_hook并检查返回值。
__init)中完成注册,并在模块退出函数(如 __exit)中执行注销。注册失败时,模块应返回错误码,避免加载后无法正常工作。
4.2 nf_unregister_net_hook API 详解
nf_unregister_net_hook 用于将之前注册的钩子函数从处理链中移除。该函数会同步等待所有正在执行该钩子的 CPU 核心完成处理,确保在返回后不会有任何数据包路径仍持有该钩子引用。
4.2.1 函数原型与参数
void nf_unregister_net_hook(struct net *net,
const struct nf_hook_ops *reg);
| 参数 | 类型 | 描述 |
|---|---|---|
net |
struct net * |
与注册时相同的网络命名空间指针。 |
reg |
const struct nf_hook_ops * |
指向与注册时相同的 nf_hook_ops 结构体。内核通过比较结构体地址来定位要移除的钩子。 |
4.2.2 注销注意事项
- 必须使用同一结构体指针: 注销时传入的
reg指针必须与注册时完全一致,内核通过指针地址匹配,而非字段内容。 - 同步等待: 该函数内部调用
synchronize_net,确保所有 CPU 上的钩子执行完毕后才返回。这意味着注销函数可能阻塞,不能在原子上下文中调用。 - 重复注销: 对未注册的钩子调用注销会导致内核警告(
WARN_ON),但不会造成系统崩溃。
nf_hook_ops 实例进行注销。前者会导致钩子悬空,内核在卸载模块后仍可能调用已释放的函数指针,引发内核崩溃;后者会导致注销失败,钩子残留。
4.3 钩子优先级机制
Netfilter 在每个钩子点上维护一个按优先级排序的钩子函数链表。优先级决定了数据包经过该钩子点时,各个钩子函数的执行顺序。优先级值越小,执行越靠前。
4.3.1 优先级定义
优先级使用 int 类型表示,取值范围为 INT_MIN 到 INT_MAX。内核预定义了一些标准优先级常量,位于 <linux/netfilter_ipv4.h> 和 <linux/netfilter_ipv6.h> 中。
| 优先级常量 | 值 | 典型用途 |
|---|---|---|
NF_IP_PRI_FIRST |
INT_MIN |
最优先执行,通常用于特殊跟踪或调试。 |
NF_IP_PRI_CONNTRACK |
-200 |
连接跟踪模块(conntrack)使用。 |
NF_IP_PRI_MANGLE |
-150 |
mangle 表操作。 |
NF_IP_PRI_NAT_DST |
-100 |
目标 NAT(DNAT)。 |
NF_IP_PRI_FILTER |
0 |
filter 表默认优先级。 |
NF_IP_PRI_SECURITY |
50 |
安全模块(如 SELinux)使用。 |
NF_IP_PRI_NAT_SRC |
100 |
源 NAT(SNAT)。 |
NF_IP_PRI_LAST |
INT_MAX |
最后执行,通常用于统计或日志。 |
4.3.2 优先级冲突与解决
当多个钩子函数注册到同一钩子点且优先级相同时,内核按照注册顺序(FIFO)执行。如果希望确保特定顺序,应显式设置不同的优先级值。
NF_IP_PRI_FILTER - 1(即 -1)表示在 filter 之前执行。
4.4 钩子注册与执行流程 SVG 图
下图展示了从模块初始化到数据包经过钩子点的完整流程,包括注册、链表插入、优先级排序和执行顺序。
4.5 完整代码示例:注册与注销
以下代码展示了一个完整的 Netfilter 钩子模块,包含注册、注销和钩子函数实现。
#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>
static struct nf_hook_ops nfho;
static unsigned int my_hook_func(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph;
struct tcphdr *tcph;
if (!skb)
return NF_ACCEPT;
iph = ip_hdr(skb);
if (!iph)
return NF_ACCEPT;
/* 仅处理 TCP 数据包 */
if (iph->protocol != IPPROTO_TCP)
return NF_ACCEPT;
tcph = tcp_hdr(skb);
if (!tcph)
return NF_ACCEPT;
/* 丢弃目标端口为 23(Telnet)的数据包 */
if (ntohs(tcph->dest) == 23) {
printk(KERN_INFO "myhook: Dropping Telnet packet\n");
return NF_DROP;
}
return NF_ACCEPT;
}
static int __init my_hook_init(void)
{
int ret;
nfho.hook = my_hook_func;
nfho.pf = NFPROTO_IPV4;
nfho.hooknum = NF_INET_PRE_ROUTING;
nfho.priority = NF_IP_PRI_FILTER - 1; /* 在 filter 之前执行 */
ret = nf_register_net_hook(&init_net, &nfho);
if (ret) {
printk(KERN_ERR "myhook: Failed to register hook, error %d\n", ret);
return ret;
}
printk(KERN_INFO "myhook: Hook registered successfully\n");
return 0;
}
static void __exit my_hook_exit(void)
{
nf_unregister_net_hook(&init_net, &nfho);
printk(KERN_INFO "myhook: Hook unregistered\n");
}
module_init(my_hook_init);
module_exit(my_hook_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Netfilter Course");
MODULE_DESCRIPTION("Example Netfilter hook module");
4.6 优先级选择策略 SVG 图
下图展示了不同优先级值在钩子链表中的位置关系,以及如何选择自定义优先级以避免冲突。
4.7 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 模块卸载后系统崩溃 | 钩子未注销或注销时使用了不同的 nf_hook_ops 指针 |
确保 nf_unregister_net_hook 使用与注册时完全相同的结构体地址 |
| 钩子函数未被执行 | 优先级设置过高或过低,被其他钩子提前返回 NF_DROP 或 NF_STOLEN |
检查优先级值,使用 NF_IP_PRI_FIRST 临时测试 |
注册返回 -EEXIST |
同一钩子点、同一优先级已存在相同钩子 | 检查是否重复注册,或调整优先级值 |
| 注销时内核警告 | 尝试注销未注册的钩子 | 在注销前检查钩子状态,或确保注册与注销成对出现 |
钩子函数中访问 skb 导致空指针 |
未对 skb 进行空指针检查 |
始终在钩子函数开头检查 skb 是否为 NULL |
printk 打印数据包信息时,注意控制日志频率,避免 printk 导致系统性能下降。可以使用 net_ratelimit() 函数限制日志输出速率。