《再次深入到ip_conntrack的conntrack full问题》最后的一个问题提示
ip_conntrack有一个event机制,可以主动通报ip_conntrack的一些事件,包括追踪信息到期删除等事件,通知给谁呢?当然是通知给所有感兴趣的模块了,其中之一就是用户态进程,这样用户态进程得知可以采取一些措施,比如防火墙上设置一些放过规则等,这个通知机制使用了观察者设计模式。
Linux ip_conntrack的一些细节问题
由于Linux的ip_conntrack具有大量的状态,而每一种状态都有一定的超时时间,这些状态中的个别可以和网络协议的不同状态建立映射关系,另一些则不能。如果协议本身是有状态的,那么就很方便建立一种映射关系,反之如果协议没有状态,那么就不能建立映射关系。有时候,对于无状态的协议而言,ip_conntrack的状态超时时间会带来一些令人郁闷的问题。
总之,Linux的ip_conntrack机制如果深究起来还真的看点,如果搞防火墙开发,实属不可不察也。下面就举几个例子。
例子举例
例子1:
对于UDP而言,它本身没有状态,无需建立连接,无需确认,纯粹就是一个数据报协议,因此ip_conntrack使用经验值来设定各个状态的超时时间,但是如果双方有一段时间没有发包,那么当初始接收端再发起一个数据包时,就会给防火墙上的基于ctstate的过滤规则带来影响,具体参见《 再次深入到ip_conntrack的conntrack full问题》。
例子2:
对于UDP而言,如果在Linux防火墙上使用NAT,那么在数据通信期间,即使NAT规则被删除或者被修改,该数据流依然会使用老的NAT规则而不是不使用任何规则或者使用新的规则。
例子3:
在早期的内核上,加载ip_conntrack模块,然后ping一个可以ping通的地址,则在/proc/net/ip_conntrack中却看不到该连接的追踪信息,而ping一个根本不可达的地址,反而能看到一个反方向为UNREPLY的追踪信息。值得注意的是,起码在2.6.32内核上,这个问题不再存在,而在2.6.9内核上还是存在的,具体哪个版本修正了它,没有详细看内核的ChangeLog。
例子4:
对于TCP而言,只要一个连接断开了,/proc/net/ip_conntrack中的关于该连接的追踪信息将马上被删除,而不会像UDP那样保留。
针对以上问题的一些解释
例子1的解释:
这个没有什么好说的,根本原因就是UDP本身没有状态,而ip_conntrack将establish状态强加给了一个UDP连接,所谓的ip_conntrack的establish状态对于所有的协议都是说有去有回的流,当然对于UDP更是这样。对于TCP而言,ip_conntrack将不是syn状态的流量都映射成了establisd状态(注意不是TCP的established状态),这也符合上述定义。在ip_conntrack处理的入口的最后:
if (set_reply) set_bit(IPS_SEEN_REPLY_BIT, &ct->status);
这说明只要set_reply为真就会修改ct的一个状态位,而set_reply在ip_conntrack_in的resolve_normal_ct调用中就会被设置。
//只要收到反向的包,就会设置IP_CT_ESTABLISHED,且把set_reply设置为1,然后返回到ip_conntrack_in的时候,就会导致ct->status的IPS_SEEN_REPLY_BIT的设置 if (DIRECTION(h) == IP_CT_DIR_REPLY) { *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY; /* Please set reply bit if this packet OK */ *set_reply = 1; } else { /* Once we've had two way comms, always ESTABLISHED. */ //只要有IPS_SEEN_REPLY_BIT位被置位,那么就是IP_CT_ESTABLISHED if (test_bit(IPS_SEEN_REPLY_BIT, &h->ctrack->status)) { DEBUGP("ip_conntrack_in: normal packet for %p\n", h->ctrack); *ctinfo = IP_CT_ESTABLISHED; ...
因此可见IP_CT_ESTABLISHED状态和具体的协议是无关的,对于TCP而言,所有SYN后面的包都会是IP_CT_ESTABLISHED状态。然而由于TCP本身拥有可以监控连接的状态,比如close-wait等,因此它在ip_conntrack中又有一些子状态,这个用于在适当的时候释放ip_conntrack数据结构,因此只要TCP的ip_conntrack的time-wait子状态到期,其ip_conntrack数据结构就会被当即释放,这一切正是因为TCP将其协议状态映射成了ip_conntrack的子状态,而这些子状态知道什么时候一个tcp流结束了。然而这一切对于UDP以及ICMP而言就没有这么幸运了,它们没有所谓的子状态,它们只能使用大胆的ip_conntrack状态。
例子2的解释:
Linux的iptables/Netfilter实现的NAT是有状态NAT,它只对一个流的头包查询NAT表,并将查询结果设置给属于该流的ip_conntrack数据结构中,一个流的头包此后的所有数据包都使用这个结果。加之UDP没有状态,ip_conntrack除非等到establish状态到期,否则无法释放(其实它根本不知道该UDP流什么时候会结束,就连establish的到期时间也是一个经验值)ip_conntrack数据结构,既然不释放该数据结构,那么头包保存的NAT结果就一直有效,因此才会出现这样的问题。对于ICMP而言,较特殊,不同的内核版本是不同的,这就是例子3中的情形。
例子3的解释:
在2.6.9的内核上,icmp_packet如下:
static int icmp_packet(struct ip_conntrack *ct, const struct sk_buff *skb, enum ip_conntrack_info ctinfo) { /* Try to delete connection immediately after all replies: won't actually vanish as we still have skb, and del_timer means this will only run once even if count hits zero twice (theoretically possible with SMP) */ //只要有包返回,则递减icmp流的引用计数,如果是0,则释放ip_conntrack连接。实际上非SMP情况下在resolve_normal_ct中总会将找到的ip_conntrack的引用计数加1的,如果此时到达以下语句,即使icmp.count为0,调用了timeout.function,也不会释放ip_conntrack,进而在filter表中仍然可以使用ctsate或者state这些match,直到相关联的skb被free之后才会调用ip_conntrack_put进而将连接追踪的引用计数变为0从而删除之。 if (CTINFO2DIR(ctinfo) == IP_CT_DIR_REPLY) { //如果有同一个源到同一个目的的icmp包或者其返回包经过则递增icmp.count字段。 if (atomic_dec_and_test(&ct->proto.icmp.count) && del_timer(&ct->timeout)) ct->timeout.function((unsigned long)ct); } else { atomic_inc(&ct->proto.icmp.count); ip_ct_refresh_acct(ct, ctinfo, skb, ip_ct_icmp_timeout); } return NF_ACCEPT; }
该函数被ip_conntrack_in回调。实际上,对于每一个协议都有一个类似这样的回调函数,名为packet,该回调函数处理和协议相关的内容,比如对于TCP而言就是处理子状态。
看了上面的代码分析,我们得知对于可以ping通的地址,由于返回包很快到达进而可以清除ip_conntrack数据结构,然而又因为其引用计数递减后不为0从而不被释放,则其不影响filter表中的--state判断,然而一旦相关的skb离开内核则会释放skb,进而递减1后的ip_conntrack引用计数为0而被释放,连接追踪数据结构随即被释放,这就是低版本(包括2.6.9)内核的做法,所以,当能ping通时,连接追踪信息很快被释放,你很难在/proc/net/ip_conntrack中看到它,而当你ping一个不可达的地址时,由于没有返回包,其连接追踪信息反而能被展现,虽然其返回包的状态为NOREPLEY。
现在想想,为何Linux内核对待ICMP没有像UDP那样,毕竟它们都是一类的,直接驾于IP之上,没有连接,没有确认,没有状态,为何不同呢?原因在于,UDP无论如何也是体现了一种双向或者单向的通信,而ICMP则只是在传达一种消息而已,一般而言,没有ICMP长时间占据一个通信流的,一般都是一个来回或者有去无回之类的,这就是它们的本质区别,因此对于ICMP,一个来回完了,连接追踪信息也就删除了,这很合理,看起来没什么不好,然而看看其对长ping的处理,对于长ping,ip_conntrack要不断地删除连接追踪,然后建立新的连接追踪...如此反复,消耗了大量的CPU资源,看起来ip_conntrack对于ICMP的做法有利于静态空间的优化,也即是它最小化了内存空间的占用,然而它却最大化了CPU时间的占用,内存如此之便宜的今天,这样有意义吗?无论如何,高一点的版本朝相反的方向改变了这一点,高版本的内核提出了另一种优化,那就是对CPU时间的优化,最终划归成了和UDP一样的处理方式,设置了一个ICMP超时时间。
起码在2.6.32版本的内核中,ip_conntrack就不再和2.6.9内核一样地处理ICMP了,而是和UDP一样地处理。
例子4的解释:
通过例子1的解释,我想这个已经不难理解了。
总结一下
Linux的ip_conntrack机制是很多基于状态的配置策略的基石,它的状态基于一个经验测定的超时时间,该时间过期后将会删除相应的连接追踪信息。这就是所谓的Linux的“基于状态”。然而网络协议的状态不一定都能和ip_conntrack的状态一一对应得上,因此ip_conntrack不得不自己定义状态的含义。
本质上讲,ip_conntrack要应对两类协议,一类是有连接的协议,一类是无连接的协议。对于由连接的协议,由于协议本身就知道何时拆除一个连接,因此ip_conntrack也就知道何时删除一个ip_conntrack数据结构,然而对于无连接的协议,协议本身并不知道何时结束一个流,因此ip_conntrack也只能根据经验值来估算之。特别要注意的是,由于ip_conntrack是全程无监控的,因此即便对于TCP这样有连接的协议来讲,其处在一个特定状态不变时,ip_conntrack仍然无法得知其何时离开该状态,因此对于诸如TCP之类的有状态协议而言,在特定的某一个协议状态(ip_conntrack子状态)之内,其行为和无状态的诸如UDP的协议一模一样,比如虽然TCP的establish设置为5天已经够长了,但是超过5天不传输数据,然后反方向主动传输数据的情形也会存在。