洞悉linux下的Netfilter&iptables --- 2

如何理解Netfilter中的连接跟踪机制?

    本篇我打算以一个问句开头,因为在知识探索的道路上只有多问然后充分调动起思考的机器才能让自己走得更远。连接跟踪定义很简单:用来记录和跟踪连接的状态。

问:为什么又需要连接跟踪功能呢?

答:因为它是状态防火墙NAT的实现基础。

OK,算是明白了。Neftiler为了实现基于数据连接状态侦测的状态防火墙功能和NAT地址转换功能才开发出了连接跟踪这套机制。那就意思是说:如果编译内核时开启了连接跟踪选项,那么Linux系统就会为它收到的每个数据包维持一个连接状态用于记录这条数据连接的状态。接下来我们就来研究一下Netfilter的连接跟踪的设计思想和实现方式。

    之前有一副图,我们可以很明确的看到:用于实现连接跟踪入口 hook 函数以较高的优先级分别被注册到了 netfitler NF_IP_PRE_ROUTING NF_IP_LOCAL_OUT 两个 hook 点上;用于实现连接跟踪出口 hook 函数以非常低的优先级分别被注册到了 netfilter NF_IP_LOCAL_IN NF_IP_POST_ROUTING 两个 hook 点上。

其实PRE_ROUTING和LOCAL_OUT点可以看作是整个netfilter的入口,而POST_ROUTING和LOCAL_IN可以看作是其出口。在只考虑连接跟踪的情况下,一个数据包无外乎有以下三种流程可以走:

一、发送给本机的数据包

流程:PRE_ROUTING----LOCAL_IN---本地进程

二、需要本机转发的数据包

流程:PRE_ROUTING---FORWARD---POST_ROUTING---外出

三、从本机发出的数据包

流程:LOCAL_OUT----POST_ROUTING---外出


我们都知道在INET层用于表示数据包的结构是大名鼎鼎的sk_buff{}(后面简称skb),如果你不幸的没听说过这个东东,那么我强烈的建议你先补一下网络协议栈的基础知识再继续阅读这篇文章。在skb中有个成员指针nfct,类型是struct nf_conntrack{},该结构定义在include/linux/skbuff.h文件中。该结构记录了连接记录被公开应用的计数,也方便其他地方对连接跟踪的引用。连接跟踪在实际应用中一般都通过强制类型转换将nfct转换成指向ip_conntrack{}类型(定义在include/linux/netfilter_ipv4/ip_conntrack.h里)来获取一个数据包所属连接跟踪的状态信息的。即:Neftilter框架用ip_conntrack{}来记录一个数据包与其连接的状态关系

同时在include/linux/netfilter_ipv4/ip_conntrack.h文件中还提供了一个非常有用的接口:struct ip_conntrack *ip_conntrack_get(skb, ctinfo)用于获取一个skb的nfct指针,从而得知该数据包的连接状态和该连接状态的相关信息ctinfo。从连接跟踪的角度来看,这个ctinfo表示了每个数据包的几种连接状态:

l  IP_CT_ESTABLISHED

Packet是一个已建连接的一部分,在其初始方向。

l  IP_CT_RELATED

Packet属于一个已建连接的相关连接,在其初始方向。

l  IP_CT_NEW

Packet试图建立新的连接

l  IP_CT_ESTABLISHED+IP_CT_IS_REPLY

Packet是一个已建连接的一部分,在其响应方向。

l  IP_CT_RELATED+IP_CT_IS_REPLY

Packet属于一个已建连接的相关连接,在其响应方向。

    在连接跟踪内部,收到的每个skb首先被转换成一个ip_conntrack_tuple{}结构,也就是说ip_conntrack_tuple{}结构才是连接跟踪系统所“认识”的数据包。那么skbip_conntrack_tuple{}结构之间是如何转换的呢?这个问题没有一个统一的答案,与具体的协议息息相关。例如,对于TCP/UDP协议,根据“源、目的IP+源、目的端口”再加序列号就可以唯一的标识一个数据包了;对于ICMP协议,根据“源、目的IP+类型+代号”再加序列号才可以唯一确定一个ICMP报文等等。对于诸如像FTP这种应用层的“活动”协议来说情况就更复杂了。本文不试图去分析某种具体协议的连接跟踪实现,而是探究连接跟踪的设计原理和其工作流程,使大家掌握连接跟踪的精髓。因为现在Linux内核更新的太快的都到3.4.x,变化之大啊。就算是2.6.22和2.6.21在连接跟踪这块还是有些区别呢。一旦大家理解了连接跟踪的设计思想,掌握了其神韵,它再怎么也万变不离其宗,再看具体的代码实现时就不会犯迷糊了。俗话说“授人一鱼,不如授人一渔”,我们教给大家的是方法。有了方法再加上自己的勤学苦练,那就成了技能,最后可以使得大家在为自己的协议开发连接跟踪功能时心里有数。这也是我写这个系列博文的初衷和目的。与君共勉。

在开始分析连接跟踪之前,我们还是站在统帅的角度来俯视一下整个连接跟踪的布局。这里我先用比较粗略的精简流程图为大家做个展示,目的是方便大家理解,好入门。当然,我的理解可能还有不太准确的地方,还请大牛们帮小弟指正。

    我还是重申一下:连接跟踪分入口和出口两个点。谨记:入口时创建连接跟踪记录,出口时将该记录加入到连接跟踪表中。我们分别来看看。

入口:

整个入口的流程简述如下:对于每个到来的skb,连接跟踪都将其转换成一个tuple结构,然后用该tuple去查连接跟踪表。如果该类型的数据包没有被跟踪过,将为其在连接跟踪的hash表里建立一个连接记录项,对于已经跟踪过了的数据包则不用此操作。紧接着,调用该报文所属协议的连接跟踪模块的所提供的packet()回调函数,最后根据状态改变连接跟踪记录的状态。

出口:

整个出口的流程简述如下:对于每个即将离开Netfilter框架的数据包,如果用于处理该协议类型报文的连接跟踪模块提供了helper函数,那么该数据包首先会被helper函数处理,然后才去判断,如果该报文已经被跟踪过了,那么其所属连接的状态,决定该包是该被丢弃、或是返回协议栈继续传输,又或者将其加入到连接跟踪表中。


连接跟踪的协议管理:

    我们前面曾说过,不同协议其连接跟踪的实现是不相同的。每种协议如果要开发自己的连接跟踪模块,那么它首先必须实例化一个ip_conntrack_protocol{}结构体类型的变量,对其进行必要的填充,然后调用ip_conntrack_protocol_register()函数将该结构进行注册,其实就是根据协议类型将其设置到全局数组ip_ct_protos[]中的相应位置上。

     ip_ct_protos变量里保存连接跟踪系统当前可以处理的所有协议,协议号作为数组唯一的下标,如下图所示。

    结构体ip_conntrack_protocol{}中的每个成员,内核源码已经做了很详细的注释了,这里我就不一一解释了,在实际开发过程中我们用到了哪些函数再具体分析。


  连接跟踪的辅助模块:

    Netfilter的连接跟踪为我们提供了一个非常有用的功能模块:helper。该模块可以使我们以很小的代价来完成对连接跟踪功能的扩展。这种应用场景需求一般是,当一个数据包即将离开Netfilter框架之前,我们可以对数据包再做一些最后的处理。从前面的图我们也可以看出来,helper模块以较低优先级被注册到了Netfilter的LOCAL_OUT和POST_ROUTING两个hook点上。

每一个辅助模块都是一个ip_conntrack_helper{}结构体类型的对象。也就是说,如果你所开发的协议需要连接跟踪辅助模块来完成一些工作的话,那么你必须也去实例化一个ip_conntrack_helper{}对象,对其进行填充,最后调用ip_conntrack_helper_register{}函数将你的辅助模块注册到全局变量helpers里,该结构是个双向链表,里面保存了当前已经注册到连接跟踪系统里的所有协议的辅助模块。

全局helpers变量的定义和初始化在net/netfilter/nf_conntrack_helper.c文件中完成的。

最后,我们的helpers变量所表示的双向链表一般都是像下图所示的这样子:

由此我们基本上就可以知道,注册在Netfilter框架里LOCAL_OUTPOST_ROUTING两个hook点上ip_conntrack_help()回调函数所做的事情基本也就很清晰了:那就是通过依次遍历helpers链表,然后调用每个ip_conntrack_helper{}对象的help()函数。


期望连接:

    Netfilter的连接跟踪为支持诸如FTP这样的“活动”连接提供了一个叫做“期望连接”的机制。我们都知道FTP协议服务端用21端口做命令传输通道,主动模式下服务器用20端口做数据传输通道;被动模式下服务器随机开一个高于1024的端口,然后客户端来连接这个端口开始数据传输。也就是说无论主、被动,都需要两条连接:命令通道的连接和数据通道的连接。连接跟踪在处理这种应用场景时提出了一个“期望连接”的概念,即一条数据连接和另外一条数据连接是相关的,然后对于这种有“相关性”的连接给出自己的解决方案。我们说过,本文不打算分析某种具体协议连接跟踪的实现。接下来我们就来谈谈期望连接。

    每条期望连接都用一个ip_conntrack_expect{}结构体类型的对象来表示,所有的期望连接存储在由全局变量ip_conntrack_expect_list所指向的双向链表中,该链表的结构一般如下:

         结构体ip_conntrack_expect{}中的成员及其意义在内核源码中也做了充分的注释,这里我就不逐一介绍了,等到需要的时候再详细探讨。


连接跟踪表:

    说了半天终于到我们连接跟踪表抛头露面的时候了。连接跟踪表是一个用于记录所有数据包连接信息的hash散列表,其实连接跟踪表就是一个以数据包的hash值组成的一个双向循环链表数组,每条链表中的每个节点都是ip_conntrack_tuple_hash{}类型的一个对象。连接跟踪表是由一个全局的双向链表指针变量ip_conntrack_hash[]来表示。为了使我们更容易理解ip_conntrack_hash[]这个双向循环链表的数组,我们将前面提到的几个重要的目前还未介绍的结构ip_conntrack_tuple{}ip_conntrack{}ip_conntrack_tuple_hash{}分别介绍一下。

    我们可以看到ip_conntrack_tuple_hash{}仅仅是对ip_conntrack_tuple{}的封装而已,将其组织成了一个双向链表结构。因此,在理解层面上我们可以认为它们是同一个东西。

在分析ip_conntrack{}结构时,我们将前面所有和其相关的数据结构都列出来,方便大家对其理解和记忆。

该图可是说是连接跟踪部分的数据核心,接下来我们来详细说说ip_conntrack{}结构中相关成员的意义。

l  ct_general:该结构记录了连接记录被公开应用的计数,也方便其他地方对连接跟踪的引用。

l  status:数据包连接的状态,是一个比特位图。

l  timeout:不同协议的每条连接都有默认超时时间,如果在超过了该时间且没有属于某条连接的数据包来刷新该连接跟踪记录,那么会调用这种协议类型提供的超时函数。

l  counters:该成员只有在编译内核时打开了CONFIG_IP_NF_CT_ACCT开完才会存在,代表某条连接所记录的字节数和包数。

l  master:该成员指向另外一个ip_conntrack{}。一般用于期望连接场景。即如果当前连接是另外某条连接的期望连接的话,那么该成员就指向那条我们所属的主连接。

l  helper:如果某种协议提供了扩展模块,就通过该成员来调用扩展模块的功能函数。

l  proto:该结构是ip_conntrack_proto{}类型,和我们前面曾介绍过的用于存储不同协议连接跟踪的ip_conntrack_protocol{}结构不要混淆了。前者是个枚举类型,后者是个结构体类型。这里的proto表示不同协议为了实现其自身的连接跟踪功能而需要的一些额外参数信息。目前这个枚举类型如下:

   如果将来你的协议在实现连接跟踪时也需要一些额外数据,那么可以对该结构进行扩充。

l  help:该成员代表不同的应用为了实现其自身的连接跟踪功能而需要的一些额外参数信息,也是个枚举类型的ip_conntrack_help{}结构,和我们前面刚介绍过的结构体类型ip_conntrack_helpers{}容易混淆。ip_conntrack_proto{}是为协议层需要而存在的,而ip_conntrack_help{}是为应用层需要而存在

l  tuplehash:该结构是个ip_conntrack_tuple_hash{}类型的数组,大小为2。tuplehash[0]表示一条数据流“初始”方向上的连接情况,tuplehash[1]表示该数据流“应答”方向的响应情况,见上图所示。

    到目前为止,我们已经了解了连接跟踪设计思想和其工作机制:连接跟踪是Netfilter提供的一套基础框架,不同的协议可以根据其自身协议的特殊性在连接跟踪机制的指导和约束下来开发本协议的连接跟踪功能,最后将其交给连接跟踪机制来统一管理。

    未完,待续…

Netfilter连接跟踪的详细流程

    上一篇我们了解了连接跟踪的基本框架和大概流程,本篇我们着重分析一下,数据包在连接跟踪系统里的旅程,以达到对连接跟踪运行原理深入理解的目的。

    连接跟踪机制在Netfilter框架里所注册的hook函数一共就五个:ip_conntrack_defrag()、ip_conntrack_in()、ip_conntrack_local()、ip_conntrack_help()

和ip_confirm()。前几篇博文中我们知道ip_conntrack_local()最终还是调用了ip_conntrack_in()。这五个hook函数及其挂载点,想必现在大家应该也已经烂熟于心了,如果记不起来请看【上】篇博文。

    在连接跟踪的入口处主要有三个函数在工作:ip_conntrack_defrag()、ip_conntrack_in()、ip_conntrack_local();在出口处就两个:ip_conntrack_help()和ip_confirm()。

接下来的事情就变得非常奇妙,请大家将自己当作一个需要转发的数据包,且是一条新的连接。然后跟随我去连接跟踪里耍一圈吧。在进入连接跟踪之前,我需要警告大家:连接跟踪虽然不会改变数据包本身,但是它可能会将数据包丢弃。


我们的旅行的线路图已经有了:

ip_conntrack_defrag()

    当我们初到连接跟踪门口的时候,是这位小生来招待我们。这个函数主要是完成IP报文分片的重新组装,将属于一个IP报文的多个分片重组成一个真正的报文。关于IP分片,大家可以去阅读《TCP/IP详解卷1》了解一点基础,至于IP分片是如何被重新组装一个完整的IP报文也不是我们的重心,这里不展开讲。该函数也向我们透露了一个秘密,那就是连接跟踪只跟踪完整的IP报文,不对IP分片进行跟踪,所有的IP分片都必须被还原成原始报文,才能进入连接跟踪系统。

 

ip_conntrack_in()

     该函数的核心是 resolve_normal_ct() 函数所做的事情,其执行流程如下所示:

在接下来的分析中,需要大家对上一篇文章提到的几个数据结构:

ip_conntrack{}、ip_conntrack_tuple{}、ip_conntrack_tuple_hash{}和ip_conntrack_protocol{}以及它们的关系必须弄得很清楚,你才能彻底地读懂resolve_normal_ct()函数是干什么。最好手头再有一份2.6.21的内核源码,然后打开source insight来对照着阅读效果会更棒!

第一步:ip_conntrack_in()函数首先根据数据包skb的协议号,在全局数组ip_ct_protos[]中查找某种协议(如TCP,UDP或ICMP等)所注册的连接跟踪处理模块ip_conntrack_protocol{},如下所示。

在结构中,具体的协议必须提供将属于它自己的数据包skb转换成ip_conntrack_tuple{}结构的回调函数pkt_to_tuple()和invert_tuple(),用于处理新连接的new()函数等等。

第二步:找到对应的协议的处理单元proto后,便调用该协议提供的错误校验函数(如果该协议提供的话)error来对skb进行合法性校验。

    第三步:调用 resolve_normal_ct() 函数。该函数的重要性不言而喻,它承担连接跟踪入口处剩下的所有工作。该函数根据 skb 中相关信息,调用协议提供的 pkt_to_tuple() 函数生成一个 ip_conntrack_tuple{} 结构体对象 tuple 。然后用该 tuple 去查找连接跟踪表,看它是否属于某个 tuple_hash{} 链。请注意,一条连接跟踪由两条 ip_conntrack_tuple_hash{} 链构成,一“去”一“回”,参见上一篇博文末尾部分的讲解。为了使大家更直观地理解连接跟踪表,我将画出来,如下图,就是个双向链表的数组而已。

如果找到了该tuple所属于的tuple_hash链表,则返回该链表的地址;如果没找到,表明该类型的数据包没有被跟踪,那么我们首先必须建立一个ip_conntrack{}结构的实例,即创建一个连接记录项。

然后,计算tuple的应答repl_tuple,对这个ip_conntrack{}对象做一番必要的初始化后,其中还包括,将我们计算出来的tuple和其反向tuple的地址赋给连接跟踪ip_conntrack里的tuplehash[IP_CT_DIR_ORIGINAL]和tuplehash[IP_CT_DIR_REPLY]。

最后,把ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL]的地址返回。这恰恰是一条连接跟踪记录初始方向链表的地址。Netfilter中有一条链表unconfirmed,里面保存了所有目前还没有收到过确认报文的连接跟踪记录,然后我们的ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL]就会被添加到unconfirmed链表中。

第四步:调用协议所提供的packet()函数,该函数承担着最后向Netfilter框架返回值的使命,如果数据包不是连接中有效的部分,返回-1,否则返回NF_ACCEPT。也就是说,如果你要为自己的协议开发连接跟踪功能,那么在实例化一个ip_conntrack_protocol{}对象时必须对该结构中的packet()函数做仔细设计。

虽然我不逐行解释代码,只分析原理,但有一句代码还是要提一下。

resolve_normal_ct()函数中有一行ct = tuplehash_to_ctrack(h)的代码,参见源代码。其中h是已存在的或新建立的ip_conntrack_tuple_hash{}对象,ct是ip_conntrack{}类型的指针。不要误以为这一句代码的是在创建ct对象,因为创建的工作在init_conntrack()函数中已经完成。本行代码的意思是根据ip_conntrack{}结构体中tuplehash[IP_CT_DIR_ORIGINAL]成员的地址,反过来计算其所在的结构体ip_conntrack{}对象的首地址,请大家注意。

大家也看到ip_conntrack_in()函数只是创建了用于保存连接跟踪记录的ip_conntrack{}对象而已,并完成了对其相关属性的填充和状态的设置等工作。简单来说,我们这个数据包目前已经拿到连接跟踪系统办法的“绿卡”ip_conntrack{}了,但是还没有盖章生效。

 

ip_conntrack_help()

大家只要把我前面关于钩子函数在五个HOOK点所挂载情况的那张图记住,就明白ip_conntrack_help()函数在其所注册的hook点的位置了。当我们这个数据包所属的协议在其提供的连接跟踪模块时已经提供了ip_conntrack_helper{}模块,或是别人针对我们这种协议类型的数据包提供了扩展的功能模块,那么接下来的事儿就很简单了:

首先,判断数据包是否拿到“绿卡”,即连接跟踪是否为该类型协议的包生成了连接跟踪记录项ip_conntrack{};

其次,该数据包所属的连接状态不属于一个已建连接的相关连接,在其响应方向。

两个条件都成立,就用该helper模块提供的help()函数去处理我们这个数据包skb。最后,这个help()函数也必须向Netfilter框架返回NF_ACCEPT或NF_DROP等值。任意一个条件不成立则ip_conntrack_help()函数直接返回NF_ACCEPT,我们这个数据包继续传输。

 

ip_confirm()

    该函数是我们离开Netfilter时遇到的最后一个家伙了,如果我们这个数据包已经拿到了“绿卡”ip_conntrack{},并且我们这个数据包所属的连接还没收到过确认报文,并且该连接还未失效。然后,我们这个ip_confirm()函数要做的事就是:

    拿到连接跟踪为该数据包生成ip_conntrack{}对象,根据连接“来”、“去”方向tuple计算其hash值,然后在连接跟踪表ip_conntrack_hash[]见上图中查找是否已存在该tuple。如果已存在,该函数最后返回NF_DROP;如果不存在,则将该连接“来”、“去”方向tuple插入到连接跟踪表ip_conntrack_hash[]里,并向Netfilter框架返回NF_ACCEPT。之所以要再最后才将连接跟踪记录加入连接跟踪表是考虑到数据包可能被过滤掉。

    至此,我们本次旅行就圆满结束了。这里我们只分析了转发报文的情况。发送给本机的报文流程与此一致,而对于所有从本机发送出去的报文,其流程上唯一的区别就是在调用ip_conntrack_in()的地方换成了ip_conntrack_local()函数。前面说过,ip_conntrack_local()里面其实调用的还是ip_conntrack_in()。ip_conntrack_local()里只是增加了一个特性:那就是对于从本机发出的小数据包不进行连接跟踪。

    未完,待续

连接跟踪系统的初始化流程分析

    有了前面的知识,我们再分析连接跟踪系统的初始化ip_conntrack_standalone_init()函数就太容易不过了。还是先上ip_conntrack_standalone_init()函数的流程图:

该函数的核心上图已经标出来了“初始化连接跟踪系统”和“注册连接跟踪的hook函数”。其他两块这里简单做个普及,不展开讲。至少让大家明白连接跟踪为什么需要两中文件系统。

1、  procfs(/proc文件系统)

这是一个虚拟的文件系统,通常挂载在/proc,允许内核以文件的形式向用户空间输出内部信息。该目录下的所有文件并没有实际存在在磁盘里,但可以通过cat、more或>shell重定向予以写入,这些文件甚至可以像普通文件那样指定其读写权限。创建这些文件的内核组件可以说明任何一个文件可以由谁读取或写入。但是:用户不能在/proc目录下新增,移除文件或目录

2、  sysctl(/proc/sys目录)

此接口允许用户空间读取或修改内核变量的值。不能用此接口对每个内核变量进行操作:内核应该明确指出哪些变量从此接口对用户空间是可见的。从用户空间,你可以用两种方式访问sysctl输出的变量:sysctl系统调用接口;procfs。当内核支持procfs文件系统时,会在/proc中增加一个特殊目录(/proc/sys),为每个由sysctl所输出的内核变量引入一个文件,我们通过对这些文件的读写操作就可以影响到内核里该变量的值了。

    除此之外还有一种sysfs文件系统,这里就不介绍了,如果你感兴趣可以去研读《Linux设备驱动程序》一书的详细讲解。

    那么回到我们连接跟踪系统里来,由此我们可以知道:连接跟踪系统向用户空间输出一些内核变量,方便用户对连接跟踪的某些特性加以灵活控制,如改变最大连接跟踪数、修改TCP、UDP或ICMP协议的连接跟踪超时是时限等等。

     注意一点: /proc/sys 目录下的任何一个文件名,对应着内核中有一个一模一样同名的内核变量。例如,我的系统中该目录是如下这个样子:

ip_conntrack_init()函数

    该函数承担了连接跟踪系统初始化的绝大部分工作,其流程我们也画出来了,大家可以对照源码来一步一步分析。

    第一步:连接跟踪的表大小跟系统内存相关,而最大连接跟踪数和连接跟踪表容量的关系是:最大连接跟踪数=8×连接跟踪表容量。代码中是这样的:

ip_conntrack_max = 8 × ip_conntrack_htable_size;那么从上面的图我们可以看出来,我们可以通过手工修改/proc/sys/net/ipv4/netfilter目录下同名的ip_conntrack_max文件即可动态修改连接系统的最大连接跟踪数了。

    第二步:注册Netfilter所用的sockopt,先不讲,以后再说。只要知道是这里注册的就行了。

    第三步:为连接跟踪hash表ip_conntrack_hash分配内存并进行初始化。并创建连接跟踪和期望连接跟踪的高速缓存。

     第四步:将 TCP UDP ICMP 协议的连接跟踪协议体,根据不同协议的协议号,注册到全局数组 ip_ct_protos[] 中,如下所示:

    最后再做一些善后工作,例如注册DROP这个target所需的功能函数,为其他诸如NAT这样的模块所需的参数ip_conntrack_untracked做初始化,关于这个参数我们在NAT模块中再详细讨论它。

    这样,我们连接跟踪系统的初始化工作就算彻底完成了。有了前几篇关于连接跟踪的基础知识,再看代码是不是有种神清气爽,豁然开朗的感觉。

    至于连接跟踪系统所提供的那五个hook函数的注册,我想现在的你应该连都不用看就知道它所做的事情了吧。

     未完,待续

基于连接跟踪机制的状态防火墙的设计与实现

连接跟踪本身并没有实现什么具体功能,它为状态防火墙和NAT提供了基础框架。前面几章节我们也看到:从连接跟踪的职责来看,它只是完成了数据包从“个性”到“共性”抽象的约定,即它的核心工作是如何针对不同协议报文而定义一个通用的“连接”的概念出来,具体的实现由不同协议自身根据其报文特殊性的实际情况来提供。那么连接跟踪的主要工作其实可以总结为:入口处,收到一个数据包后,计算其hash值,然后根据hash值查找连接跟踪表,如果没找到连接跟踪记录,就为其创建一个连接跟踪项;如果找到了,则返回该连接跟踪项。出口处,根据实际情况决定该数据包是被还给协议栈继续传递还是直接被丢弃。

    我们先看一下iptables指南中关于用户空间中数据包的四种状态及其解释:

状态

解释

NEW

NEW说明这个包是我们看到的第一个包。意思就是,这是conntrack模块看到的某个连接第一个包,它即将被匹配了。比如,我们看到一个SYN包,是我们所留意的连接的第一个包,就要匹配它。第一个包也可能不是SYN包,但它仍会被认为是NEW状态。这样做有时会导致一些问题,但对某些情况是有非常大的帮助的。例如,在我们想恢复某条从其他的防火墙丢失的连接时,或者某个连接已经超时,但实际上并未关闭时。

ESTABLISHED

ESTABLISHED已经注意到两个方向上的数据传输,而且会继续匹配这个连接的包。处于ESTABLISHED状态的连接是非常容易理解的。只要发送并接到应答,连接就是ESTABLISHED的了。一个连接要从NEW变为ESTABLISHED,只需要接到应答包即可,不管这个包是发往防火墙的,还是要由防火墙转发的。ICMP的错误和重定向等信息包也被看作是ESTABLISHED,只要它们是我们所发出的信息的应答。

RELATED

RELATED是个比较麻烦的状态。当一个连接和某个已处于ESTABLISHED状态的连接有关系时,就被认为是RELATE的了。换句话说,一个连接要想是RELATED的,首先要有一个ESTABLISHED的连接。这个ESTABLISHED连接再产生一个主连接之外的连接,这个新的连接就是RELATED的了,当然前提是conntrack模块要能理解RELATED。ftp是个很好的例子,FTP-data 连接就是和FTP-control有RELATED的。还有其他的例子,比如,通过IRC的DCC连接。有了这个状态,ICMP应答、FTP传输、DCC等才能穿过防火墙正常工作。注意,大部分还有一些UDP协议都依赖这个机制。这些协议是很复杂的,它们把连接信息放在数据包里,并且要求这些信息能被正确理解。

INVALID

INVALID说明数据包不能被识别属于哪个连接或没有任何状态。有几个原因可以产生这种情况,比如,内存溢出,收到不知属于哪个连接的ICMP 错误信息。一般地,我们DROP这个状态的任何东西。

    认真体会这个表格所表达意思对我们理解状态防火墙的机制和实现有很大的帮助。我们以最常见的TCP、UDP和ICMP协议为例来分析,因为他们最常见。对于TCP/UDP来说,我们可以用“源/目的IP+源/目的端口”唯一的标识一条连接;因为ICMP没有端口的概念,因此对ICMP而言,其“连接”的表示方法为“源/目的IP+类型+代码+ID”。因此,你就可以明白,如果你有一种不同于目前所有协议的新协议要为其开发连接跟踪功能,那么你必须定以一个可以唯一标识该报文的规格,这是必须的。

    接下来我就抛砖引玉,分析一下NEW、ESTABLISHED、RELATED和INVALID几种状态内核中的变迁过程。


     针对于NEW状态的理解:

依旧在ip_conntrack_in()函数中,只不过我们这次的侧重点不同。由于该报文是某条连接的第一个数据包,ip_conntrack_find_get()函数中根据该数据包的tuple在连接跟踪表ip_conntrack_hash中肯定找不到对应的连接跟踪记录,然后重任就交给了init_conntrack()函数:

如果连接跟踪数已满,或没有足够的内存时,均会返回错误。否则,将新连接跟踪记录的引用计数置为1,设置连接跟踪记录“初始”和“应答”方向的tuple链,同时还设置了连接跟踪记录被销毁和超时的回调处理函数destroy_conntrack()和death_by_timeout()等。

    至此,我们新的连接记录ip_conntrack{}就华丽丽滴诞生了。每种协议必须对其“新连接记录项”提供一个名为new()的回调函数。该函数的主要作用就是针对不同协议,什么样的报文才被称为“new”状态必须由每种协议自身去考虑和实现。具体我就不深入分析了,大家只要知道这里有这么一出戏就可以了,感兴趣的朋友可以去研究研究。当然,这需要对协议字段和意义有比较透彻清晰的了解才能完全弄明白别人为什么要那么设计。毕竟我们不是去为TCP、UDP或ICMP开发连接跟踪,开源界的信条就是“永远不要重复发明车轮”。如果你想深入研究现有的东西,目的只有一个:那就是学习别人的优点和长处,要有重点,有主次的去学习,不然会让自己很累不说,还会打击求知的积极性和动力。

闲话不都说,我们继续往下分析。如果该数据包所属的协议集提供了helper接口,那么将其挂到conntrack->helper的回调接口上。最后,将该连接跟踪项“初始”方向的tuple链添加到一条名为unconfirmed的全局链表中,该链表里存储的都是截止到目前为止还未曾收到“应答”方向数据包的连接跟踪记录。

费了老半天劲儿,状态防火墙终于出来和大家见面了:

ip_ct_get_tuple()函数里初始化时tuple.dst.dir就被设置为了IP_CT_DIR_ORIGINAL,因为我们讨论的就是NEW状态的连接,tuple.dst.dir字段到目前为止还未被改变过,与此同时,ip_conntrack.status位图自从被创建之日起经过memset()操作后就一直为全0状态,才有最后的skb->nfctinfo=*ctinfo=IP_CT_NEW和skb->nfct = &ip_conntrack->ct_general。

继续回到ip_conntrack_in()函数里,此时调用协议所提供的回调packet()函数。在博文六中我们曾提及过,该函数承担着数据包生死存亡的使命。这里我们有必要注意一下packet()函数最后给Netfilter框架返回值的一些细节:

-1,其实就是-NF_ACCEPT,意思是:连接跟踪出错了,该数据包不是有效连接的一部分,Netfilter不要再对这类报文做跟踪了,调用前面的回调函数destroy()清除已经为其设置的连接跟踪项记录项,释放资源。最后向Netfilter框架返回ACCEPT,让该数据包继续传输。

0,就是NF_DROP,返回给Netfilter框架的也是该值,那么这数据包就挂在这里了。

1,就是NF_ACCEPT,同样,该数据包已经被正确跟踪了,通知Netfilter框架继续传输该数据包。

对于像TCP这样非常复杂的协议才用到了NF_DROP操作,像UDP、ICMP、GRE、SCTP等协议都没有到,但不排除你的项目中使用NF_DROP的情形。

在连接跟踪的出口处的ip_conntrack_confirm()函数中,如果已经为该数据包skb创建了连接跟踪记录ip_conntrack{}(即skb->nfct有值),则做如下处理:

如果该连接还没有收到回复报文----明显如此;

如果该连接没有挂掉----毫无疑问。

因为是新连接,因此在全局链表数组 ip_conntrack_hash[] 就没有记录该连接“初始”和“应答”方向的 tuplehash 链。然后,紧接着我把该连接初始方向的 tuplehash ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL].list unconfirmed 链表上卸下来。并且,将该连接初始和应答方向的 tuplehash 链表根据其各自的 hash 之加入到 ip_conntrack_hash[] 里,最后启动连接跟踪老化时间定时器,修改引用计数,将 ip_conntrack.status 状态位图更新为 IPS_CONFIRMED_BIT ,并向 Netfilter 框架返回 NF_ACCEPT 。数据包离开 Netfilter 继续在协议栈中传递。

针对于ESTABLISHED状态的理解:

    每个ip_conntrack{}结构末尾有两条tuplehash链,分别代表“初始”和“应答”方向的数据流向,如下图所示:

    如果一条连接进入ESTABLISHED,那么的前一状态一定是NEW。因此,我们继续前面的分析分析过程,当我们的连接跟踪记录收到了其对应的响应报文后的处理流程。注意前面刚分析过的:对于新连接的状态位图status已经被设置成IPS_CONFIRMED_BIT了。

    继续在ip_conntrack_in()函数,所以数据包从skb到tuple的生成过程中,初始化时都有tuple->dst.dir = IP_CT_DIR_ORIGINAL;那么tuple->dst.dir是何时被改变状态的呢?这就牵扯到一种很重要的通信机制netlink。连接跟踪框架还为连接记录的跃迁改变定义了一些事件处理和通知机制,而这目前不是本文的重点。在入口处,虽然从连接跟踪表中找到了该tuple所属的连接跟踪记录项,但在过滤表中该报文有可能会被丢弃,因此不应该急于改变回应报文所属的连接跟踪记录的状态,回应报文也有skb->nfctinfo = *ctinfo=IP_CT_NEW,该数据包所属的连接跟踪记录保存在skb->nfct里。在出口处,函数ip_conntrack_confirm()中,由于在NEW状态时,连接跟踪记录项中status=IPS_CONFIRMED_BIT了,因此这里对于响应报文,不会重复执行函数__ip_conntrack_confirm()。

紧接着,当“回应”报文被连接跟踪框架看到后,它会调用ip_ct_deliver_cached_events()函数,以某种具体的事件通过netlink机制来通知ip_conntrack_netlink.c文件中的ctnetlink_parse_tuple()函数将初始方向的tuple->dst.dir = IP_CT_DIR_REPLY。这里理解起来稍微有点抽象,不过还是提醒大家抓重点思路,我们后面这几章内容相对来说比前面几章要稍微复杂些,这也是能力提升必须要经历的过程。

    至此,该连接跟踪记录 ip_conntrack{} 的数据我们来欣赏一下:

    如果该连接后续一个初始方向的数据包又到达了,那么在resolve_normal_ct()函数中,便会执行设置skb->nfctinfo=*ctinfo =IP_CT_ESTABLISHED + IP_CT_IS_REPLY和set_reply=1,然后退出到ip_conntrack_in()里,将上图中ip_conntrack{}结构体里的status成员属性由原来的IPS_CONFIRMED_BIT设置为IPS_SEEN_REPLY_BIT,并通过函数ip_conntrack_event_cache()触发一个netlink状态改变事件。最后,在ip_conntrack_confirm()里也只出发netlink事件而已。紧接着,第二个应答方向的报文也到达了,和上面的处理动作一样。

 

    针对于RELATED状态的理解:

    很多文章都从FTP协议的角度来剖析这个状态,确实FTP也是最能体现RELATED特性的协议。假如有个报文属于某条已经处于ESTABLISHED状态的连接,我们来看看状态防火墙是如何来识别这种情况。

    依然在resolve_normal_ct()函数中,执行到init_conntrack()里面时,通过数据包相对应的tuple即可在全局链表ip_conntrack_expect_list里找到该连接所属的主连接。然后将我们这条RELATED连接记录的status= IPS_EXPECTED_BIT,并建立我们RELATED连接和它所属的主连接之间的对应关系conntrack->master = exp->master,同样将其挂载到unconfirmed链里。返回到resolve_normal_ct()里,为数据包设置状态值skb->nfctinfo = *ctinfoIP_CT_RELATED。当数据包将要离开时,在ip_conntrack_confirm()函数中也会将其加入连接跟踪表,并设置status为IPS_CONFIRMED_BIT。剩下的流程就和前面我们讨论的是一样了,唯一却别的地方在于属于RELATED的连接跟踪,其master指向了它所属的主连接跟踪记录项。

    INVALID状态压根儿就没找着,汗。跟俺玩躲猫猫,哥还不鸟你捏。。。

    本篇的知识点相对来说总体来说比较抽象,内容比较多,实现上也较为复杂,我在省略了其状态跃迁流程情况下都还写了这么多东西,很多地方研究的其实都不是很深入,只能感慨Netfilter的博大。

    未完,待续…


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值