231 Netfilter:Linux 网络过滤核心架构与 HOOK 机制深度解析
前言
在 Linux 内核的网络子系统深处,有一套被誉为”网络防火墙脊梁”的核心框架——Netfilter。无论是 iptables、nftables,还是 Docker 网络、Kubernetes Service 的实现,都离不开 Netfilter 的 HOOK 机制提供的基础能力。理解 Netfilter,是掌握 Linux 网络安全的必要前提。
本文将深入剖析 Netfilter 的框架设计、HOOK 注册机制、数据包处理流程,并通过可运行的内核模块 Demo 展示 HOOK 的实际工作方式,同时对比 Windows WFP(Winsock Filtering Platform)的实现差异,帮助你在异构系统间迁移网络安全逻辑时少走弯路。
1. Netfilter 框架概述
1.1 什么是 Netfilter?
Netfilter 是 Linux 内核 2.4.x(2001年)引入的一个通用数据包过滤框架。它在内核网络协议栈的关键路径上设置了若干”钩子点”(HOOK),允许内核模块在数据包经过时介入检查、修改、丢弃或接受。
Netfilter 的设计哲学可以总结为三点:
- 无侵入:不修改内核网络栈的核心逻辑,只通过 HOOK 挂载
- 可叠加:多个模块可以挂载同一个 HOOK,顺序执行
- 状态无关:基础框架无状态,具体规则由上层的 iptables 等工具维护
1.2 核心组件
Netfilter 由以下几个核心组件构成:
┌─────────────────────────────────────────────────────────────┐
│ Netfilter 架构图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────────────┐ ┌──────────┐ │
│ │ ingress │───▶│ NF_HOOK_PRE │───▶│ Routing │ │
│ │ netdev │ │ (PREROUTING) │ │ Decision │ │
│ └──────────┘ └─────────────────┘ └────┬─────┘ │
│ │ │
│ ┌───────────────────────────────────┤ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ INPUT Hook │ │ FORWARD Hook │ │
│ │ (LOCAL_IN) │ │ (FORWARD) │ │
│ └──────┬───────┘ └───────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Application │ │ OUTPUT Hook │ │
│ │ (Receive) │ │ (POSTROUTING) │ │
│ └──────────────┘ └────────┬─────────┘ │
│ │ │
│ ┌─────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ egress netdev │ │
│ └──────────────────┘ │
│ │
│ Hook 点说明: │
│ • NF_INET_PRE_ROUTING : 路由决策前(DNAT常用) │
│ • NF_INET_LOCAL_IN : 发往本机的包 │
│ • NF_INET_FORWARD : 不是本地且需要转发的包 │
│ • NF_INET_LOCAL_OUT : 本机产生的出站包 │
│ • NF_INET_POST_ROUTING : 离开前(SNAT常用) │
│ │
└─────────────────────────────────────────────────────────────┘1.3 五类 HOOK 点详解
Linux 内核在数据包穿越网络协议栈的完整路径上,定义了五个标准 HOOK 点:
| HOOK 点 | 枚举值 | 触发时机 | 典型应用场景 |
|---|---|---|---|
NF_INET_PRE_ROUTING |
NF_IP_PRE_ROUTING |
接收数据包后,路由决策之前 | DNAT、端口镜像 |
NF_INET_LOCAL_IN |
NF_IP_LOCAL_IN |
数据包确定发往本机之后 | 防火墙 INPUT 规则 |
NF_INET_FORWARD |
NF_IP_FORWARD |
数据包需要转发而非本地消费 | 防火墙 FORWARD 规则 |
NF_INET_LOCAL_OUT |
NF_IP_LOCAL_OUT |
本机产生的数据包,路由决策之后 | 防火墙 OUTPUT 规则 |
NF_INET_POST_ROUTING |
NF_IP_POST_ROUTING |
数据包即将发出网卡之前 | SNAT、流量整形 |
2. HOOK 机制内部实现
2.1 数据结构
Netfilter 的核心数据结构是 struct nf_hook_ops,定义在 include/linux/netfilter.h 中:
struct nf_hook_ops {
struct list_head list; // 用于组成 hook 链表
/* 回调函数,返回处理结果 */
nf_hookfn *hook;
/* 协议族:AF_INET, AF_INET6, etc */
u_int8_t pf;
/* Hook 点:PRE_ROUTING, LOCAL_IN, etc */
unsigned int hooknum;
/* 优先级,在同一 Hook 点多个模块按优先级排序 */
int priority;
/* 私有数据指针 */
void *priv;
};关键回调类型:
typedef unsigned int nf_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state);数据包(struct sk_buff)是整个 Netfilter 世界围绕的核心对象。
2.2 Hook 注册流程
┌──────────────────────────────────────────────────────────────┐
│ Hook 注册完整流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 用户空间 │
│ ┌──────────────────────────────────────────┐ │
│ │ iptables -A INPUT -p tcp --dport 22 │ │
│ │ -j ACCEPT │ │
│ └────────────────────┬─────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ xt_register_table() │ │
│ │ netlink socket 通信内核 │ │
│ └────────────────────┬─────────────────────┘ │
│ ▼ │
│ 内核空间 │
│ ┌──────────────────────────────────────────┐ │
│ │ nf_register_net_hook() │ │
│ │ • 分配 hook 节点 │ │
│ │ • 插入对应 pf/hooknum 的链表(按优先级) │ │
│ └────────────────────┬─────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ Netfilter Hook 链表(per netns) │ │
│ │ │ │
│ │ PRE_ROUTING: [prio=INT_MIN]→[prio=0]→[prio=100] │
│ │ INPUT: [prio=INT_MIN]→[prio=0]→[prio=100] │
│ │ FORWARD: ... │ │
│ │ OUTPUT: ... │ │
│ │ POST_ROUTING: ... │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘2.3 数据包处理流程
当数据包到达网卡,Linux 内核网络栈的处理流程与 Netfilter HOOK 的关系:
packet arrives at NIC
│
▼
┌───────────────┐
│ sk_buff 分配 │ ← 创建 skb
└───────┬───────┘
▼
┌───────────────────┐ NF_INET_PRE_ROUTING hook ──┐
│ netif_receive_skb│ (mangle/dnat/filter) │
└───────┬───────────┘ │
▼ │
┌───────────────────┐ │
│ 路由判决 │ → 目的为本机? │
│ ip_rcv_finish │ │
└───────┬───────────┘ │
│ │ │
本机 转发 │
│ │ │
▼ ▼ │
NF_INET_ NF_INET_ │
LOCAL_IN FORWARD │
│ │ │
▼ ▼ │
┌──────────┐ ┌──────────┐ │
│ 本机协议 │ │ 转发路由 │ │
│ 栈处理 │ │ 决策 │ │
└────┬─────┘ └────┬─────┘ │
│ │ │
▼ ▼ │
NF_INET_ NF_INET_ │
LOCAL_OUT POST_ROUTING │
│ │ │
▼ ▼ │
┌──────────┐ ┌──────────┐ │
│ 发送到 │ │ 发送到 │ │
│ 应用层 │ │ 网卡发出 │ │
└──────────┘ └──────────┘每个 HOOK 点返回的判决值决定了数据包的下一步命运:
enum nf_hook_verdict {
NF_DROP = 0, // 丢弃数据包
NF_ACCEPT = 1, // 接受,继续传递
NF_STOLEN = 2, // 劫持,HOOK 处理后不再传递(需自己发送)
NF_QUEUE = 3, // 进入用户空间队列(nft_queue)
NF_REPEAT = 4, // 再次调用此 HOOK
NF_STOP = 5, // 停止,类似于 ACCEPT 但不再调用后续 HOOK
};3. Linux 实践:编写一个可运行的 Netfilter HOOK 模块
3.1 环境准备
# 确认内核版本(需要 4.x 或更高)
uname -r
# 安装内核头文件(Debian/Ubuntu)
sudo apt-get install linux-headers-$(uname -r) build-essential
# 创建工作目录
mkdir -p ~/netfilter_demo && cd ~/netfilter_demo3.2 完整的可运行 Demo:拦截并记录所有 ICMP 包
以下是一个完整的内核模块,实现了一个监听 ICMP 数据包的 Netfilter HOOK:
文件:nf_icmp_observer.c
/*
* Netfilter HOOK Demo: 观察并记录所有 ICMP 数据包
* 编译: make KDIR=/lib/modules/$(uname -r)/build
* 加载: sudo insmod nf_icmp_observer.ko
* 卸载: sudo rmmod nf_icmp_observer
* 验证: ping -c 3 127.0.0.1 && dmesg | grep ICMP_OBSERVER
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <linux/if_ether.h>
#include <linux/inet.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("OS Kernel Series");
MODULE_DESCRIPTION("Netfilter HOOK Observer - ICMP Packet Logger");
MODULE_VERSION("1.0");
/* 存储拦截统计 */
static unsigned long icmp_total_count = 0;
static unsigned long icmp_drop_count = 0;
/* 自定义优先级:-10(高优先级),在内置 conntrack 之前 */
#define NF_HOOK_PRIORITY_NFQUEUE -10
/* HOOK 回调函数:观察所有 ICMP 包 */
static unsigned int icmp_observer_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph;
struct icmphdr *icmph;
char src_ip[16], dst_ip[16];
/* 跳过空 skb */
if (!skb)
return NF_ACCEPT;
/* 只处理 IPv4 */
if (state->pf != NFPROTO_IPV4)
return NF_ACCEPT;
iph = ip_hdr(skb);
if (!iph)
return NF_ACCEPT;
/* 只处理 ICMP 协议 */
if (iph->protocol != IPPROTO_ICMP)
return NF_ACCEPT;
/* 获取 ICMP 头部(可能没有封装在 Ethernet 中) */
icmph = icmp_hdr(skb);
if (!icmph)
return NF_ACCEPT;
/* 转换 IP 地址 */
snprintf(src_ip, sizeof(src_ip), "%pI4", &iph->saddr);
snprintf(dst_ip, sizeof(dst_ip), "%pI4", &iph->daddr);
/* 增加计数 */
icmp_total_count++;
/* 打印日志到内核环形缓冲区 */
printk(KERN_INFO "ICMP_OBSERVER [hook=%s] %s -> %s "
"type=%u code=%u seq=%u id=%u tot_len=%un",
state->hook == NF_INET_PRE_ROUTING ? "PRE" :
state->hook == NF_INET_LOCAL_IN ? "IN" :
state->hook == NF_INET_FORWARD ? "FWD" :
state->hook == NF_INET_LOCAL_OUT ? "OUT" :
state->hook == NF_INET_POST_ROUTING ? "POST" : "UNK",
src_ip, dst_ip,
icmph->type, icmph->code,
ntohs(icmph->un.echo.sequence),
ntohs(icmph->un.echo.id),
ntohs(iph->tot_len));
/* 演示:每 10 个 ICMP 包,临时 DROP 一个(带日志) */
if (icmp_total_count % 10 == 0) {
icmp_drop_count++;
printk(KERN_WARNING "ICMP_OBSERVER: DROP #%lu (drop rate 10%%)n",
icmp_drop_count);
/* 这里返回 NF_ACCEPT,只做观察不实际丢包 */
/* 如需实际丢包:return NF_DROP; */
}
/* 返回 ACCEPT,数据包正常继续传递 */
return NF_ACCEPT;
}
/* 定义 HOOK 操作结构 */
static struct nf_hook_ops icmp_observer_ops = {
.hook = icmp_observer_hook,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING, /* 可改为其他 HOOK 点测试 */
.priority = NF_IP_PRI_FIRST, /* 高优先级 */
};
/* 可选:注册多个 HOOK,监听所有相关点 */
static struct nf_hook_ops icmp_observer_ops_out = {
.hook = icmp_observer_hook,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_FIRST,
};
/* 模块初始化 */
static int __init icmp_observer_init(void)
{
int ret;
printk(KERN_INFO "ICMP_OBSERVER: module loading, version 1.0n");
/* 注册 PRE_ROUTING HOOK */
ret = nf_register_net_hook(&init_net, &icmp_observer_ops);
if (ret < 0) {
printk(KERN_ERR "ICMP_OBSERVER: failed to register PRE_ROUTING hook: %dn", ret);
return ret;
}
/* 注册 LOCAL_OUT HOOK */
ret = nf_register_net_hook(&init_net, &icmp_observer_ops_out);
if (ret < 0) {
printk(KERN_ERR "ICMP_OBSERVER: failed to register LOCAL_OUT hook: %dn", ret);
nf_unregister_net_hook(&init_net, &icmp_observer_ops);
return ret;
}
printk(KERN_INFO "ICMP_OBSERVER: registered %s hook at PRE_ROUTING + LOCAL_OUTn",
KBUILD_MODNAME);
printk(KERN_INFO "ICMP_OBSERVER: use 'dmesg -w' to observe live ICMP eventsn");
return 0;
}
/* 模块清理 */
static void __exit icmp_observer_exit(void)
{
nf_unregister_net_hook(&init_net, &icmp_observer_ops_out);
nf_unregister_net_hook(&init_net, &icmp_observer_ops);
printk(KERN_INFO "ICMP_OBSERVER: unloaded. "
"total_icmp=%lu dropped=%lun",
icmp_total_count, icmp_drop_count);
}
module_init(icmp_observer_init);
module_exit(icmp_observer_exit);Makefile:
obj-m += nf_icmp_observer.o
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
.PHONY: all clean3.3 编译和运行
cd ~/netfilter_demo
# 编译模块
make
# 查看生成的文件
ls -la *.ko # 应该看到 nf_icmp_observer.ko
# 查看模块信息
modinfo nf_icmp_observer.ko
# 加载模块(需要 root)
sudo insmod nf_icmp_observer.ko
# 清除内核日志
sudo dmesg -c
# 在另一个终端 ping 本地
ping -c 5 127.0.0.1
# 查看拦截结果
sudo dmesg
# 卸载模块
sudo rmmod nf_icmp_observer预期输出示例:
ICMP_OBSERVER: module loading, version 1.0
ICMP_OBSERVER: registered nf_icmp_observer hook at PRE_ROUTING + LOCAL_OUT
ICMP_OBSERVER [hook=PRE] 127.0.0.1 -> 127.0.0.1 type=8 code=0 seq=1 id=12345 tot_len=64
ICMP_OBSERVER [hook=PRE] 127.0.0.1 -> 127.0.0.1 type=8 code=0 seq=2 id=12345 tot_len=64
...
ICMP_OBSERVER: unloaded. total_icmp=10 dropped=03.4 Hook 优先级实测
注册同一 HOOK 点、不同优先级的多个模块时,内核按照优先级升序(数值越小优先级越高)执行:
# 查看已注册的 Netfilter hooks(需要 perf tools)
cat /proc/net/netfilter/nf_cache4. Windows 实现差距:WFP 与 Netfilter
在 Windows 系统中,提供类似功能的是 WFP(Winsock Filtering Platform),从 Windows Vista(2006)开始引入,替代了更早的 LSP(Layered Service Provider)和 NDIS 过滤驱动。
4.1 架构对比
┌──────────────────────┬──────────────────────────────────────┐
│ Netfilter │ Windows WFP │
├──────────────────────┼──────────────────────────────────────┤
│ 出现年份 │ Windows Vista (2006) │
├──────────────────────┼──────────────────────────────────────┤
│ 编程模型 │ 内核模块(.sys)+ 用户态 Filter │
├──────────────────────┼──────────────────────────────────────┤
│ 状态管理 │ 无内置(由 iptables/conntrack 提供) │
├──────────────────────┼──────────────────────────────────────┤
│ 配置工具 │ iptables/nftables CLI │
├──────────────────────┼──────────────────────────────────────┤
│ Hook 点 │ 5 个固定 HOOK 点 │
├──────────────────────┼──────────────────────────────────────┤
│ 数据包修改能力 │ 全部(Mangle/DNAT/SNAT) │
├──────────────────────┼──────────────────────────────────────┤
│ 扩展性 │ 通过内核模块无限扩展 │
└──────────────────────┴──────────────────────────────────────┘4.2 WFP 的 Callout 机制
WFP 中,Filter 被称为 Callout,通过 FwpsCalloutRegister 注册。WFP 的过滤层级比 Netfilter 更细,分为多个 Layer(对应不同网络层):
┌─────────────────────────────────────────────────────────────┐
│ Windows WFP Filter Layers │
├─────────────────────────────────────────────────────────────┤
│ │
│ Application Layer FWPM_LAYER_APPLICATION │
│ (AFD 层面,socket 级别) FWPM_LAYER_ALE_AUTH │
│ FWPM_LAYER_ALE_FLOW │
│ │
│ Network Layer FWPM_LAYER_INBOUND_IPPACKETS │
│ (IP 级别) FWPM_LAYER_OUTBOUND_IPPACKETS │
│ FWPM_LAYER_IPFORWARD │
│ FWPM_LAYER_INBOUND_IPPACKETS_V4 │
│ │
│ Transport Layer FWPM_LAYER_TCP_DISPATCH │
│ FWPM_LAYER_UDP_DISPATCH │
│ │
│ Data Link Layer FWPM_LAYER_INBOUND_MAC_FRAME │
│ (MAC 级别) FWPM_LAYER_OUTBOUND_MAC_FRAME │
│ │
└─────────────────────────────────────────────────────────────┘WFP 的显著优势在于与 Windows 基础设施的深度集成(IPSec、Teredo、NAT 遍历);显著劣势是 API 复杂度远高于 Netfilter,调试工具链不完善,且无法像 Linux 一样用简单的 shell 命令配置规则。
4.3 迁移对照表
| Netfilter 概念 | Windows WFP 等价 | 说明 |
|---|---|---|
| HOOK 点 | Layer + SubLayer | WFP 更细粒度 |
nf_register_hook |
FwpsCalloutRegister |
注册 Callout |
struct sk_buff |
TX_NET_BUFFER_LIST / NET_BUFFER |
数据结构不同 |
NF_DROP |
FWP_ACTION_FLAG_DROP |
判决方式不同 |
| iptables CLI | netsh wfp / WFP API |
配置方式完全不同 |
| tc (traffic control) | QOS / Traffic shaping |
流控独立子系统 |
5. 深层原理:Hook 如何嵌入协议栈
Netfilter 通过 netfilterNf_hook 函数嵌入内核网络协议栈:
// 简化版调用链
ip_rcv(skb)
→ nf_hook(skb, PF_INET, NF_INET_PRE_ROUTING, dev_net(skb->dev), NULL, ip_rcv_finish)
→ nf_hook_slow(skb, head)
→ list_for_each_entry(ops, head, list)
→ ops->hook(ops->priv, skb, state)
// 返回 verdict
→ switch(verdict) { NF_DROP: kfree_skb, ... }nf_hook 是内核网络协议栈与 Netfilter 框架之间的边界函数。它接受一个 nf_hook_entries 链表,按优先级遍历执行所有已注册的 HOOK 函数。这使得 Netfilter 在不修改协议栈主体逻辑的前提下,获得了对数据包的完全控制权。
结语
Netfilter 是 Linux 网络安全体系的地基。它的 HOOK 机制简洁而强大——5 个 HOOK 点,统一的 sk_buff 接口,清晰的 verdict 返回值——支撑了从个人防火墙到电信级网关的广泛场景。理解 HOOK 注册、数据包遍历、优先级排序的原理,是掌握整个 Linux 网络安全体系的钥匙。
下一篇文章我们将进入 iptables 的世界,看看如何基于 Netfilter 提供的高层规则配置接口,构建真正可用的防火墙策略。
📢 公众号:Kernel Hacker
🔍 系列:操作系统内核深度探索 | 第 231 篇
📅 发布日期:2025-07-15
🔗 原创内容,转载须获授权 | 禁止匿名转载
💬 留言区:你用过哪些 Netfilter HOOK 模块?欢迎分享经验
评论