基于连接跟踪机制的状态防火墙的设计与实现
连接跟踪本身并没有实现什么具体功能,它为状态防火墙和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几种状态内核中的变迁过程。
依旧在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 = *ctinfo= IP_CT_RELATED。当数据包将要离开时,在ip_conntrack_confirm()函数中也会将其加入连接跟踪表,并设置status为IPS_CONFIRMED_BIT。剩下的流程就和前面我们讨论的是一样了,唯一却别的地方在于属于RELATED的连接跟踪,其master指向了它所属的主连接跟踪记录项。
INVALID状态压根儿就没找着,汗。跟俺玩躲猫猫,哥还不鸟你捏。。。
本篇的知识点相对来说总体来说比较抽象,内容比较多,实现上也较为复杂,我在省略了其状态跃迁流程情况下都还写了这么多东西,很多地方研究的其实都不是很深入,只能感慨Netfilter的博大。
未完,待续…