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函数总结如下:
以上便是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的原理,这里我们谈谈其实现机制的问题。
我们回过头来再分析一下前一篇提及的那个用于存储不同协议族在每个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;
};
下面我们对该结构体的成员参数进行简单的解释:
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中相应的过滤点,我们要做的工作其实很简单:
这也是最原生的扩展方式。有了上面这个对nf_hook_ops及其用法的分析,后面我们再分析其他模块,如filter模块、nat模块时就不会那么难懂了。
内核在网络协议栈的关键点引入NF_HOOK宏,从而搭建起了整个Netfilter框架。但是,NF_HOOK宏仅仅只是一个跳转而已,更重要的内容是“内核是如何注册钩子函数的呢?这些钩子函数又是如何被调用的呢?谁来维护和管理这些钩子函数呢?”
未完待续。。。