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 典型注册流程

  1. 定义 nf_hook_ops 结构体实例,通常使用静态全局变量。
  2. 填充 hook 字段为自定义钩子函数指针。
  3. 设置 pf 字段为协议族,如 NFPROTO_IPV4。
  4. 设置 hooknum 字段为目标钩子点,如 NF_INET_PRE_ROUTING。
  5. 设置 priority 字段,决定在钩子链中的位置。
  6. 调用 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)执行。如果希望确保特定顺序,应显式设置不同的优先级值。

建议: 自定义模块应避免使用内核预定义的优先级值,除非明确需要与 conntrack、NAT 等模块交互。推荐使用介于两个标准优先级之间的值,例如 NF_IP_PRI_FILTER - 1(即 -1)表示在 filter 之前执行。

4.4 钩子注册与执行流程 SVG 图

下图展示了从模块初始化到数据包经过钩子点的完整流程,包括注册、链表插入、优先级排序和执行顺序。

钩子注册与执行流程 模块初始化 填充 nf_hook_ops nf_register_net_hook 按优先级插入链表 数据包到达钩子点 遍历钩子链表 按优先级执行钩子 返回 NF_ACCEPT/NF_DROP 模块卸载 nf_unregister_net_hook 关键说明 注册时按优先级插入链表; 数据包到达后按优先级顺序 执行钩子函数。

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 图

下图展示了不同优先级值在钩子链表中的位置关系,以及如何选择自定义优先级以避免冲突。

钩子优先级链表结构 链表头 优先级 -200 优先级 -150 优先级 -100 自定义优先级 -50 优先级 0 (filter) 优先级 50 优先级选择策略 1. 使用标准优先级常量(如 NF_IP_PRI_FILTER)确保与内核模块兼容。 2. 自定义优先级应避开标准值,使用标准值 ± 偏移量,如 NF_IP_PRI_FILTER - 1。 3. 优先级值越小越先执行;相同优先级按注册顺序执行。 4. 调试阶段可使用 NF_IP_PRI_FIRST 或 NF_IP_PRI_LAST 确保最先或最后执行。

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() 函数限制日志输出速率。