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 由以下几个核心组件构成:

Bash
┌─────────────────────────────────────────────────────────────┐
│                    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 中:

C
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;
};

关键回调类型:

C
typedef unsigned int nf_hookfn(void *priv,
                               struct sk_buff *skb,
                               const struct nf_hook_state *state);

数据包(struct sk_buff)是整个 Netfilter 世界围绕的核心对象。

2.2 Hook 注册流程

Bash
┌──────────────────────────────────────────────────────────────┐
│              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 的关系:

Bash
    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 点返回的判决值决定了数据包的下一步命运:

C
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 环境准备

Bash
# 确认内核版本(需要 4.x 或更高)
uname -r

# 安装内核头文件(Debian/Ubuntu)
sudo apt-get install linux-headers-$(uname -r) build-essential

# 创建工作目录
mkdir -p ~/netfilter_demo && cd ~/netfilter_demo

3.2 完整的可运行 Demo:拦截并记录所有 ICMP 包

以下是一个完整的内核模块,实现了一个监听 ICMP 数据包的 Netfilter HOOK:

文件:nf_icmp_observer.c

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:

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 clean

3.3 编译和运行

Bash
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

预期输出示例:

Bash
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=0

3.4 Hook 优先级实测

注册同一 HOOK 点、不同优先级的多个模块时,内核按照优先级升序(数值越小优先级越高)执行:

Bash
# 查看已注册的 Netfilter hooks(需要 perf tools)
cat /proc/net/netfilter/nf_cache

4. Windows 实现差距:WFP 与 Netfilter

在 Windows 系统中,提供类似功能的是 WFP(Winsock Filtering Platform),从 Windows Vista(2006)开始引入,替代了更早的 LSP(Layered Service Provider)和 NDIS 过滤驱动。

4.1 架构对比

Bash
┌──────────────────────┬──────────────────────────────────────┐
│      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(对应不同网络层):

Bash
┌─────────────────────────────────────────────────────────────┐
│              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 函数嵌入内核网络协议栈:

C
// 简化版调用链
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 模块?欢迎分享经验

最后修改: 2024年1月24日

作者

评论

发表评论

您的邮箱地址不会被公开。