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()里只是增加了一个特性:那就是对于从本机发出的小数据包不进行连接跟踪。
未完,待续…