3. Netfilter 钩子点详解
Netfilter 是 Linux 内核网络协议栈中的核心框架,它通过在关键路径上注册钩子函数,允许内核模块或内核开发者拦截、修改或丢弃网络数据包。理解这五个钩子点的位置、触发时机和典型用途,是编写高效防火墙规则的基础。
本章将逐一剖析 NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN、NF_INET_FORWARD、NF_INET_LOCAL_OUT 和 NF_INET_POST_ROUTING 五个钩子点,并结合数据包在内核协议栈中的完整流转路径进行说明。
3.1 数据包在内核中的完整流转路径
在深入每个钩子点之前,先通过一张全局流程图建立整体认知。下图展示了 IPv4 数据包从网卡到达,经过路由决策,最终被本地进程接收、转发或发送出去的完整路径,以及五个钩子点在此路径上的精确位置。
3.2 钩子点详解
3.2.1 NF_INET_PRE_ROUTING
位置:数据包从网卡进入内核协议栈后,在路由决策之前触发。
触发时机:所有入站数据包(无论目的地址是本机还是其他主机)都会经过此钩子点。
典型用途:
- DNAT(目的地址转换):修改数据包的目的 IP 地址和端口,常用于端口映射或负载均衡。
- 连接跟踪(conntrack):在此处建立连接跟踪条目,记录数据包的状态。
- 早期过滤:在路由决策前丢弃恶意数据包,减少后续处理开销。
3.2.2 NF_INET_LOCAL_IN
位置:路由决策之后,数据包目的地址为本机时触发。
触发时机:仅当数据包的目的 IP 地址是本机某个网络接口的 IP 地址,或者是本机监听的广播/组播地址时,才会经过此钩子点。
典型用途:
- 入站防火墙过滤:根据源 IP、目的端口、协议类型等规则,允许或拒绝数据包进入本地进程。
- 本地服务访问控制:限制特定外部主机对本地 SSH、HTTP 等服务的访问。
- 日志记录:记录所有发往本机的数据包信息,用于安全审计。
3.2.3 NF_INET_FORWARD
位置:路由决策之后,数据包需要转发到其他主机时触发。
触发时机:当数据包的目的 IP 地址不是本机,且本机启用了 IP 转发功能(net.ipv4.ip_forward = 1)时,数据包会经过此钩子点。
典型用途:
- 转发流量过滤:控制哪些数据包可以被本机转发,常用于构建网络防火墙或路由器。
- SNAT/DNAT 配合:在转发路径上结合 NAT 规则,实现内网主机访问外网或外网访问内网服务。
- 流量整形与限速:对转发数据包进行带宽限制或 QoS 标记。
3.2.4 NF_INET_LOCAL_OUT
位置:本地进程发送数据包时,在路由决策之前触发。
触发时机:任何由本机进程生成的出站数据包(包括响应数据包)都会经过此钩子点。
典型用途:
- 出站防火墙过滤:限制本机进程访问外部网络,防止恶意软件外联或数据泄露。
- SNAT(源地址转换):修改数据包的源 IP 地址,常用于内网主机通过共享 IP 访问外网。
- 本地流量重定向:将本机发出的特定流量重定向到代理或透明网关。
3.2.5 NF_INET_POST_ROUTING
位置:数据包即将离开本机,发送到网卡之前触发。这是数据包在 Netfilter 框架中的最后一个钩子点。
触发时机:所有出站数据包(包括转发数据包和本地进程发出的数据包)在路由决策之后、发送到网卡之前都会经过此钩子点。
典型用途:
- SNAT(源地址转换):在数据包离开前修改源 IP 地址,确保回程数据包能正确路由。
- MASQUERADE(伪装):动态 SNAT,适用于拨号或 DHCP 获取动态 IP 的场景。
- 数据包标记与封装:在数据包离开前添加 VLAN 标签、修改 TOS 字段或进行隧道封装。
3.3 钩子点对比总结
下表从触发方向、路由决策前后关系、典型应用场景三个维度,对五个钩子点进行横向对比。
| 钩子点名称 | 触发方向 | 路由决策前后 | 典型应用场景 |
|---|---|---|---|
| NF_INET_PRE_ROUTING | 入站 | 路由决策前 | DNAT、连接跟踪、早期过滤 |
| NF_INET_LOCAL_IN | 入站 | 路由决策后 | 入站防火墙过滤、本地服务访问控制 |
| NF_INET_FORWARD | 转发 | 路由决策后 | 转发流量过滤、NAT 配合、流量整形 |
| NF_INET_LOCAL_OUT | 出站 | 路由决策前 | 出站防火墙过滤、SNAT、本地流量重定向 |
| NF_INET_POST_ROUTING | 出站 | 路由决策后 | SNAT、MASQUERADE、数据包标记与封装 |
3.4 钩子点注册示例
以下代码展示了如何在 Linux 内核模块中注册一个 Netfilter 钩子函数,并指定钩子点。该示例在 NF_INET_PRE_ROUTING 点注册,对所有入站数据包进行计数。
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/skbuff.h>
/* 钩子函数:统计所有入站数据包 */
static unsigned int pre_routing_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph;
if (!skb)
return NF_ACCEPT;
iph = ip_hdr(skb);
if (!iph)
return NF_ACCEPT;
/* 简单统计:每收到一个数据包,打印源 IP 和目的 IP */
printk(KERN_INFO "PRE_ROUTING: SRC=%pI4 DST=%pI4\n",
&iph->saddr, &iph->daddr);
/* 返回 NF_ACCEPT 表示放行数据包 */
return NF_ACCEPT;
}
/* 定义钩子点结构体 */
static struct nf_hook_ops pre_routing_ops = {
.hook = pre_routing_hook,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST,
};
/* 模块初始化 */
static int __init my_module_init(void)
{
int ret;
ret = nf_register_net_hook(&init_net, &pre_routing_ops);
if (ret < 0) {
printk(KERN_ERR "Failed to register hook\n");
return ret;
}
printk(KERN_INFO "Netfilter hook registered at PRE_ROUTING\n");
return 0;
}
/* 模块退出 */
static void __exit my_module_exit(void)
{
nf_unregister_net_hook(&init_net, &pre_routing_ops);
printk(KERN_INFO "Netfilter hook unregistered\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Netfilter Course");
MODULE_DESCRIPTION("Example: Register hook at NF_INET_PRE_ROUTING");
nf_register_net_hook()用于注册钩子函数,nf_unregister_net_hook()用于注销。hooknum字段指定钩子点,例如NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN等。priority字段决定同一钩子点上多个钩子函数的执行顺序,NF_IP_PRI_FIRST表示最高优先级。- 钩子函数返回
NF_ACCEPT表示放行数据包,返回NF_DROP表示丢弃。
3.5 钩子点选择决策树
在实际开发中,选择合适的钩子点至关重要。下面的决策树可以帮助你根据需求快速定位正确的钩子点。
3.6 常见陷阱与最佳实践
- 陷阱一:在错误的钩子点修改地址。例如,在 LOCAL_IN 中修改目的地址,此时路由决策已经完成,修改无效。正确的做法是在 PRE_ROUTING 中修改目的地址。
- 陷阱二:忽略连接跟踪的影响。连接跟踪在 PRE_ROUTING 和 LOCAL_OUT 中建立条目。如果在这两个钩子点修改数据包的关键字段(如 IP 地址、端口),必须同时更新连接跟踪条目,否则会导致回程数据包无法匹配。
- 陷阱三:在 FORWARD 钩子点过滤本地流量。FORWARD 只处理转发流量,本地流量不会经过。如果需要过滤本地入站流量,应使用 LOCAL_IN。
- 最佳实践:优先使用 iptables/nftables 等用户态工具实现常见功能,仅在需要自定义复杂逻辑或性能优化时编写内核模块。
- 最佳实践:在钩子函数中避免执行耗时操作(如睡眠、复杂计算),否则会阻塞整个网络协议栈,导致丢包或延迟飙升。