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难舍难分的缠绵可以用下图来表示:
从上图我们可以看出,ip_tables模块它是防火墙的核心模块,负责维护防火墙的规则表,通过这些规则,实现防火墙的核心功能。归纳起来,主要有三种功能:包过滤(filter)、NAT以及包处理(mangle)。同进该模块留有与用户空间通讯的接口。如第一篇博文中Netfilter处于内核中位置那副图所描述的情形。
在内核中我们习惯将上述的filter,nat和mangle等称之为模块。连接跟踪conntrack有些特殊,它是NAT模块和状态防火墙的功能基础,其实现机制我们也会在后面详细分析的。
OK,回到开篇的问题,我们来看一下基于Netfilter的防火墙系统到底定义了哪些钩子函数?而这些钩子函数都是分别挂载在哪些hook点的?按照其功能结构划分,我将这些hook函数总结如下:
包过滤子功能:包过滤一共定义了四个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 内核机制最有效,最直观的方式了,所以屡用不爽!
然后,我们拿一把大刀,从协议栈的IPv4点上顺着hook点延伸的方向一刀切下去,就会得到一个平面,如上图所示。前面这些hook函数在这个平面上的分布情况如下所示:
这幅图彻底暴露了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; }; |
对该结构体中的成员参数做一下解释:
n list:因为在一个HOOK点有可能注册多个钩子函数,因此这个变量用来将某个HOOK点所注册的所有钩子函数组织成一个双向链表;
n hook:该参数是一个指向nf_hookfn类型的函数的指针,由该函数指针所指向的回调函数在该hook被激活时调用【nf_hookfn在后面做解释】;
n owner:表示这个hook是属于哪个模块的
n pf:该hook函数所处理的协议。目前我们主要处理IPv4,所以该参数总是PF_INET;
n hooknum:钩子函数的挂载点,即HOOK点;
n 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或NF_HOOK_COND宏传递到Netfilter框架中去。
如果要增加新的钩子函数到Netfilter中相应的过滤点,我们要做的工作其实很简单:
1)、编写自己的钩子函数;
2)、实例化一个struct nf_hook_ops{}结构,并对其进行适当的填充,第一个参数list并不是用户所关心的,初始化时必须设置成{NULL,NULL};
3)、用nf_register_hook()<net/netfilter/core.c>函数将我们刚刚填充的nf_hook_ops结构体注册到相应的HOOK点上,即nf_hooks[prot][hooknum]。
这也是最原生的扩展方式。有了上面这个对nf_hook_ops{}及其用法的分析,后面我们再分析其他模块,如filter模块、nat模块时就会不那么难懂了。
内核在网络协议栈的关键点引入NF_HOOK宏,从而搭建起了整个Netfilter框架。但是NF_HOOK宏仅仅只是一个跳转而已,更重要的内容是“内核是如何注册钩子函数的呢?这些钩子函数又是如何被调用的呢?谁来维护和管理这些钩子函数?”
未完,待续…
本文出自 “心怡坊” 博客,转载请与作者联系!