2. Netfilter 框架概述
Netfilter 是 Linux 内核中一个高度模块化的包处理框架,它允许内核模块在网络协议栈的关键路径上注册回调函数,从而对经过的数据包进行过滤、修改、跟踪或丢弃。本章将深入剖析 Netfilter 的架构设计、核心钩子点、钩子函数注册机制以及优先级控制,为后续开发连接跟踪模块奠定基础。
2.1 Netfilter 架构设计
Netfilter 框架位于 Linux 网络协议栈的 IP 层(IPv4/IPv6),它通过在内核网络路径中插入一系列检查点(钩子点),将数据包处理流程从内核固定逻辑扩展为可编程的模块化流水线。其核心设计思想是 “钩子 + 优先级” 的回调机制。
架构核心组件:
- 钩子点(Hook Point): 协议栈中预定义的 5 个固定位置(NF_INET_PRE_ROUTING 等)。
- 钩子函数(Hook Function): 内核模块注册的回调,返回 NF_ACCEPT、NF_DROP 等判决。
- 优先级(Priority): 同一钩子点上多个函数的执行顺序,值越小优先级越高。
- nf_hook_ops 结构体: 注册钩子的核心数据结构,包含钩子函数指针、协议族、钩子点、优先级。
设计要点: Netfilter 的钩子机制是 同步 的——每个钩子函数必须返回一个判决结果(如 NF_ACCEPT),协议栈才会继续传递数据包。这种设计保证了包处理的可预测性,但也要求钩子函数必须高效,避免阻塞。
2.2 五大钩子点(Hook Point)详解
Netfilter 在 IPv4 协议栈中定义了 5 个标准钩子点,分别对应数据包在不同路由决策阶段的位置。理解每个钩子点的触发时机和典型用途,是正确注册钩子函数的前提。
| 钩子点枚举 | 数值 | 触发位置 | 典型用途 |
|---|---|---|---|
| NF_INET_PRE_ROUTING | 0 | 数据包进入协议栈后,路由决策之前 | 连接跟踪、DNAT、原始包过滤 |
| NF_INET_LOCAL_IN | 1 | 路由决策后,数据包发往本机 | 本机入站过滤、连接跟踪状态检查 |
| NF_INET_FORWARD | 2 | 路由决策后,数据包需要转发 | 转发过滤、带宽控制、连接跟踪 |
| NF_INET_LOCAL_OUT | 3 | 本机进程发送数据包,路由决策之前 | 本机出站过滤、SNAT、连接跟踪 |
| NF_INET_POST_ROUTING | 4 | 路由决策后,数据包离开本机前 | SNAT、出站包修改、连接跟踪确认 |
常见误区: LOCAL_OUT 钩子点位于路由决策 之前,而 POST_ROUTING 位于路由决策 之后。这意味着在 LOCAL_OUT 中修改数据包的目的地址(如 REDIRECT)会影响路由决策结果,而在 POST_ROUTING 中修改则不会。
2.3 钩子函数注册与优先级
内核模块通过 nf_register_net_hook() 或 nf_register_hook() 函数向 Netfilter 框架注册钩子。注册的核心是填充 struct nf_hook_ops 结构体,其中 priority 字段决定了同一钩子点上多个钩子函数的执行顺序。
2.3.1 nf_hook_ops 结构体
struct nf_hook_ops {
struct list_head list; // 内核链表节点,用于挂接到钩子点链表
nf_hookfn *hook; // 钩子函数指针,核心回调
struct net_device *dev; // 绑定的网络设备(NULL 表示所有设备)
void *priv; // 私有数据指针
u_int8_t pf; // 协议族,如 AF_INET、AF_INET6
unsigned int hooknum; // 钩子点编号(0-4)
int priority; // 优先级,值越小优先级越高
};
2.3.2 优先级定义与标准值
Netfilter 预定义了一组优先级常量,位于 <linux/netfilter_ipv4.h> 中。内核模块和 iptables 扩展通常使用这些常量来确保正确的执行顺序。
| 优先级常量 | 数值 | 典型使用者 |
|---|---|---|
| NF_IP_PRI_FIRST | INT_MIN | 最高优先级,极少使用 |
| NF_IP_PRI_CONNTRACK_DEFRAG | -400 | 连接跟踪分片重组 |
| NF_IP_PRI_RAW | -300 | raw 表(NOTRACK 目标) |
| NF_IP_PRI_SELINUX_FIRST | -225 | SELinux 安全策略 |
| NF_IP_PRI_CONNTRACK | -200 | 连接跟踪核心 |
| NF_IP_PRI_MANGLE | -150 | mangle 表 |
| NF_IP_PRI_NAT_DST | -100 | DNAT(目标地址转换) |
| NF_IP_PRI_FILTER | 0 | filter 表(默认过滤) |
| NF_IP_PRI_SECURITY | 50 | security 表 |
| NF_IP_PRI_NAT_SRC | 100 | SNAT(源地址转换) |
| NF_IP_PRI_SELINUX_LAST | 225 | SELinux 最后检查 |
| NF_IP_PRI_LAST | INT_MAX | 最低优先级 |
2.3.3 钩子函数注册示例
以下代码展示了一个最小化的 Netfilter 钩子模块,它在 NF_INET_PRE_ROUTING 点注册一个钩子函数,打印经过的每个数据包信息。
#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>
// 钩子函数:打印数据包源/目的 IP
static unsigned int my_hook_func(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph = ip_hdr(skb);
if (iph) {
printk(KERN_INFO "Netfilter: packet from %pI4 to %pI4\n",
&iph->saddr, &iph->daddr);
}
return NF_ACCEPT; // 放行数据包
}
// 定义钩子操作结构体
static struct nf_hook_ops my_hook_ops = {
.hook = my_hook_func,
.pf = AF_INET,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FILTER, // 与 filter 表同优先级
};
// 模块初始化
static int __init my_module_init(void)
{
int ret = nf_register_net_hook(&init_net, &my_hook_ops);
if (ret) {
printk(KERN_ERR "Failed to register hook\n");
return ret;
}
printk(KERN_INFO "Netfilter hook registered\n");
return 0;
}
// 模块退出
static void __exit my_module_exit(void)
{
nf_unregister_net_hook(&init_net, &my_hook_ops);
printk(KERN_INFO "Netfilter hook unregistered\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
2.3.4 钩子函数返回值
钩子函数必须返回一个 unsigned int 类型的判决值,Netfilter 框架根据返回值决定数据包的后续处理路径。
| 返回值 | 含义 | 行为 |
|---|---|---|
| NF_ACCEPT | 接受 | 数据包继续沿协议栈正常路径传递 |
| NF_DROP | 丢弃 | 立即释放 skb,不再传递 |
| NF_QUEUE | 排队 | 将数据包发送到用户空间处理(如 libnetfilter_queue) |
| NF_STOLEN | 窃取 | 钩子函数接管数据包所有权,框架不再处理 |
| NF_REPEAT | 重复 | 让当前钩子点重新执行(需谨慎使用,防死循环) |
开发建议: 在自定义钩子函数中,除非明确需要丢弃或重定向数据包,否则应返回
NF_ACCEPT。返回 NF_DROP 时务必记录日志(如 printk_ratelimit),以便调试。使用 NF_QUEUE 时需确保用户空间有对应的监听程序,否则数据包会丢失。