深入理解Netfilter和iptables:内核中ip_tables小觑

概要

Netfilter框架为内核模块参与IP层数据包处理提供了很大的方便,内核的防火墙模块(ip_tables)正是通过把自己所编写的一些钩子函数注册到Netfilter所监控的五个关键点(NF_IP_PRE_ROUTING/ NF_IP_LOCAL_IN/ NF_IP_FORWARD/ NF_IP_LOCAL_OUT/ NF_IP_POST_ROUTING)的这种方式介入到对数据包的处理。这些钩子函数功能非常强大,按功能可以分为四大类:连接跟踪、数据包过滤、网络地址转换(NAT)以及数据包的修改。它们之间的关系以及和Netfilter、ip_tables难分难舍的缠绵可以用下图来表示:

深入理解Netfilter和iptables:内核中ip_tables小觑_第1张图片

从上图我们可以看出,ip_tables模块是防火墙的核心模块,负责维护防火墙的规则表。通过这些规则,从而实现防火墙的核心功能。归纳起来,主要有三种功能:包过滤(filter)NAT以及包处理(mangle)。同时,该模块留有与用户空间通讯的接口。如第一篇博文中Netfilter处于内核中位置的那幅图所描述的情形。

在内核中,我们习惯于将上述的filter、nat和mangle等称之为模块。连接跟踪conntrack有些特殊,它是NAT模块和状态防火墙的功能基础,其实现机制我们在后面会详细分析。

OK,回到开篇的问题,我们来看一下基于Netfilter的防火墙系统到底定义了哪些钩子函数?而这些钩子函数都是分别挂载在哪些hook点的?按照其功能结构划分,我将这些hook函数总结如下:

深入理解Netfilter和iptables:内核中ip_tables小觑_第2张图片

  • 包过滤子功能:包过滤一共定义了四个hook函数,这四个hook函数本质最后都调用了ipt_do_table函数。
  • 网络地址转换子功能:该模块也定义了四个hook函数,其中有三个都调用了ip_nat_fn函数,ip_nat_adjust函数有自己另外的功能。
  • 连接跟踪子功能:这里的连接跟踪应该称其为一个子系统更合适些。它也定义了四个hook函数,其中ip_conntrack_local最后其实也调用了ip_conntrack_in函数。

以上便是Linux的防火墙—iptables在内核中定义的所有hook函数。接下来我们再梳理一下这些hook函数分别是被挂载在哪些hook点上的。还是先贴个三维框图,因为我觉得这个图是理解Netfilter内核机制最有效,最直观的方式了,所以屡用不爽!

深入理解Netfilter和iptables:内核中ip_tables小觑_第3张图片

然后,我们拿一把大刀,从协议栈的IPv4点上顺着hook点延伸的方向一刀切下去,就会得到一个平面,如上图所示。前面这些hook函数在这个平面上的分布情况如下所示:

深入理解Netfilter和iptables:内核中ip_tables小觑_第4张图片

这张图彻底暴露了ip_tables内核模块中hook函数在各个hook点上的分布情况。

与此同时,该图还告诉了我们很多信息:所有由网卡收上来的数据包率先被ip_conntrack_defrag处理;连接跟踪系统的入口函数以-200的优先级被注册到了PRE_ROUTING和LOCAL_OUT两个hook点上,且其优先级高于mangle操作、NAT和包过滤等其他模块;DNAT可以在PRE_ROUTING和LOCAL_OUT两个hook点上来做,SNAT可以在LOCAL_IN和POST_ROUTING两个hook点上。

如果你认真研究会发现这个图确实很有用。因为当初为了画这个图我可是两个晚上没睡好觉啊,画出来后还要验证自己的想法,就得一步一步给那些关键的hook点和hook函数分别加上调试打印信息,重新编译内核然后确认这些hook函数确实是按照我所分析的那样被调用的。因为对学术严谨就是对自己负责,一直以来我也都这么坚信的。“没有调查就没发言权”;在我们IT行业,“没有亲自动手做过就更没有发言权”。又扯远了,赶紧收回来。

框架的东西从宏观上可以使我们对整个系统的架构和设计有个比较全面的把握,接下来在分析每个细节的时候才会做到心中有数,不至于进入“盲人摸象”的难境。在本章行将结束之际,我们来看点代码级的东西。我保证只是个简单的入门了解,因为重头戏我打算放到后面,大家也知道分析代码其实最头疼,关键还是看自己的心态。

Netfilter的实现方式

在第一篇博文中,我们讲解了Netfilter的原理,这里我们谈谈其实现机制的问题。

我们回过头来再分析一下前一篇提及的那个用于存储不同协议族在每个hook点上所注册的hook函数链的二维数组nf_hooks[][],其类型为list_head:

struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];

list_head结构体定义在include/linux/list.h头文件中:

struct list_head {
           struct list_head *next, *prev;
};

这是Linux内核中处理双向链表的标准方式。当某种类型的数据结构需要被组织成双向链表时,会在该数据结构的第一个字段放置一个list_head类型的成员。在后面的使用过程中可以通过强制类型转换来实现双向链表的遍历操作。

在Netfilter中有一个非常重要的数据结构是nf_hook_ops(include/linux/netfilter.h):

struct nf_hook_ops
{
    struct list_head list;

    /* User fills in from here down. */
    nf_hookfn *hook;
    struct module *owner;
    int pf;
    int hooknum;
    /* Hooks are ordered in ascending priority. */
    int priority;
};

下面我们对该结构体的成员参数进行简单的解释:

  1. list – 因为在一个hook点可能有注册多个钩子函数,因此该变量用于将某个hook点所注册的所有钩子函数组织成一个双向链表;
  2. hook – 该参数是一个指向nf_hookfn类型的函数指针,由该函数指针所指向的回调函数在该hook被激活时调用【nf_hookfn在后面作解释】;
  3. owner – 表示这个hook是属于哪个模块;
  4. pf – 该hook函数所处理的协议。目前我们主要处理IPv4,因此该参数总是PF_INET;
  5. hooknum – 钩子函数的挂载点,即hook点;
  6. priority – 优先级。前面也说过,一个hook点可能挂在了多个钩子函数,当Netfilter在这些hook点上遍历查找所注册的钩子函数时,这些钩子函数的先后执行顺序便有该参数来指定。

nf_hookfn所定义的回调函数的原型在include/linux/netfilter.h头文件中:

typedef unsigned int nf_hookfn(
                   unsigned int hooknum,          // hook点
                   struct sk_buff **skb,              // 数据包指针
                   const struct net_device *in,   // 数据包的网络入接口
                   const struct net_device *out, // 数据包的网络出接口
                   int (*okfn)(struct sk_buff *));  // 后续的处理函数

我们可以看到,上面这五个参数最后将由NF_HOOK宏传递到Netfilter框架中去。

如果要增加新的钩子函数到Netfilter中相应的过滤点,我们要做的工作其实很简单:

  1. 编写自己的钩子函数;
  2. 实例化一个struct nf_hook_ops结构体,并对其进行适当的填充,第一个参数list并不是用户所关心的,初始化时必须设置成NULL;
  3. 调用nf_register_hook函数(net/core/netfilter.c)将我们刚刚填充的nf_hook_ops结构体变量注册到相应的hook点上,即nf_hooks[prot][hooknum];

这也是最原生的扩展方式。有了上面这个对nf_hook_ops及其用法的分析,后面我们再分析其他模块,如filter模块、nat模块时就不会那么难懂了。

内核在网络协议栈的关键点引入NF_HOOK宏,从而搭建起了整个Netfilter框架。但是,NF_HOOK宏仅仅只是一个跳转而已,更重要的内容是“内核是如何注册钩子函数的呢?这些钩子函数又是如何被调用的呢?谁来维护和管理这些钩子函数呢?”

未完待续。。。

你可能感兴趣的:(深入理解Netfilter和iptables:内核中ip_tables小觑)