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 数据包从网卡到达,经过路由决策,最终被本地进程接收、转发或发送出去的完整路径,以及五个钩子点在此路径上的精确位置。

Netfilter 五个钩子点与数据包流转路径 Netfilter 钩子点与数据包流转路径 网卡接收数据包 NF_INET_PRE_ROUTING 路由决策 NF_INET_LOCAL_IN 本地进程接收 NF_INET_FORWARD NF_INET_POST_ROUTING 网卡发送数据包 NF_INET_LOCAL_OUT 本地进程发送 说明: 红色节点为钩子点,蓝色节点为网卡接口,绿色/紫色/青色节点为路由决策或进程处理。 数据包从网卡进入后,依次经过 PRE_ROUTING → 路由决策 → (LOCAL_IN 或 FORWARD) → 最终由 POST_ROUTING 发出。 本地进程发送的数据包先经过 LOCAL_OUT,再经过路由决策,最后到达 POST_ROUTING。
关键理解:路由决策是数据包路径的分水岭。对于入站数据包,路由决策决定它是发往本机(走 LOCAL_IN)还是需要转发(走 FORWARD)。对于出站数据包,路由决策决定下一跳接口和网关。

3.2 钩子点详解

3.2.1 NF_INET_PRE_ROUTING

位置:数据包从网卡进入内核协议栈后,在路由决策之前触发。

触发时机:所有入站数据包(无论目的地址是本机还是其他主机)都会经过此钩子点。

典型用途:

  • DNAT(目的地址转换):修改数据包的目的 IP 地址和端口,常用于端口映射或负载均衡。
  • 连接跟踪(conntrack):在此处建立连接跟踪条目,记录数据包的状态。
  • 早期过滤:在路由决策前丢弃恶意数据包,减少后续处理开销。
实战案例:在 PRE_ROUTING 钩子点实现 DNAT,将外部访问 192.168.1.100:80 的数据包目的地址改为 10.0.0.2:8080。

3.2.2 NF_INET_LOCAL_IN

位置:路由决策之后,数据包目的地址为本机时触发。

触发时机:仅当数据包的目的 IP 地址是本机某个网络接口的 IP 地址,或者是本机监听的广播/组播地址时,才会经过此钩子点。

典型用途:

  • 入站防火墙过滤:根据源 IP、目的端口、协议类型等规则,允许或拒绝数据包进入本地进程。
  • 本地服务访问控制:限制特定外部主机对本地 SSH、HTTP 等服务的访问。
  • 日志记录:记录所有发往本机的数据包信息,用于安全审计。
注意:LOCAL_IN 钩子点只处理目的为本机的数据包。如果数据包是转发流量,则不会经过此钩子点。

3.2.3 NF_INET_FORWARD

位置:路由决策之后,数据包需要转发到其他主机时触发。

触发时机:当数据包的目的 IP 地址不是本机,且本机启用了 IP 转发功能(net.ipv4.ip_forward = 1)时,数据包会经过此钩子点。

典型用途:

  • 转发流量过滤:控制哪些数据包可以被本机转发,常用于构建网络防火墙或路由器。
  • SNAT/DNAT 配合:在转发路径上结合 NAT 规则,实现内网主机访问外网或外网访问内网服务。
  • 流量整形与限速:对转发数据包进行带宽限制或 QoS 标记。
关键指标:FORWARD 钩子点是实现网络隔离和访问控制的核心位置。例如,在 FORWARD 链中配置规则,允许内网访问外网,但禁止外网主动访问内网。

3.2.4 NF_INET_LOCAL_OUT

位置:本地进程发送数据包时,在路由决策之前触发。

触发时机:任何由本机进程生成的出站数据包(包括响应数据包)都会经过此钩子点。

典型用途:

  • 出站防火墙过滤:限制本机进程访问外部网络,防止恶意软件外联或数据泄露。
  • SNAT(源地址转换):修改数据包的源 IP 地址,常用于内网主机通过共享 IP 访问外网。
  • 本地流量重定向:将本机发出的特定流量重定向到代理或透明网关。
注意:LOCAL_OUT 钩子点处理的是本机主动发出的数据包,而不是响应数据包。响应数据包(如 TCP SYN-ACK)属于入站路径,会经过 LOCAL_IN。

3.2.5 NF_INET_POST_ROUTING

位置:数据包即将离开本机,发送到网卡之前触发。这是数据包在 Netfilter 框架中的最后一个钩子点。

触发时机:所有出站数据包(包括转发数据包和本地进程发出的数据包)在路由决策之后、发送到网卡之前都会经过此钩子点。

典型用途:

  • SNAT(源地址转换):在数据包离开前修改源 IP 地址,确保回程数据包能正确路由。
  • MASQUERADE(伪装):动态 SNAT,适用于拨号或 DHCP 获取动态 IP 的场景。
  • 数据包标记与封装:在数据包离开前添加 VLAN 标签、修改 TOS 字段或进行隧道封装。
注意:POST_ROUTING 是数据包离开前的最后一道关卡。在此处修改数据包时,必须确保修改后的数据包符合网络协议规范,否则可能导致通信失败。

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 钩子点选择决策树

在实际开发中,选择合适的钩子点至关重要。下面的决策树可以帮助你根据需求快速定位正确的钩子点。

钩子点选择决策树 钩子点选择决策树 数据包方向? 入站(网卡→本机) 出站(本机→网卡) 需要修改目的地址? PRE_ROUTING LOCAL_IN 需要修改源地址? POST_ROUTING LOCAL_OUT 提示:转发流量(目的非本机)应使用 FORWARD 钩子点,而非 LOCAL_IN 或 LOCAL_OUT。 如果同时需要修改地址和过滤,通常先做 NAT(PRE_ROUTING / POST_ROUTING),再做过滤(LOCAL_IN / FORWARD / LOCAL_OUT)。

3.6 常见陷阱与最佳实践

  • 陷阱一:在错误的钩子点修改地址。例如,在 LOCAL_IN 中修改目的地址,此时路由决策已经完成,修改无效。正确的做法是在 PRE_ROUTING 中修改目的地址。
  • 陷阱二:忽略连接跟踪的影响。连接跟踪在 PRE_ROUTING 和 LOCAL_OUT 中建立条目。如果在这两个钩子点修改数据包的关键字段(如 IP 地址、端口),必须同时更新连接跟踪条目,否则会导致回程数据包无法匹配。
  • 陷阱三:在 FORWARD 钩子点过滤本地流量。FORWARD 只处理转发流量,本地流量不会经过。如果需要过滤本地入站流量,应使用 LOCAL_IN。
  • 最佳实践:优先使用 iptables/nftables 等用户态工具实现常见功能,仅在需要自定义复杂逻辑或性能优化时编写内核模块。
  • 最佳实践:在钩子函数中避免执行耗时操作(如睡眠、复杂计算),否则会阻塞整个网络协议栈,导致丢包或延迟飙升。