《再次深入到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);
- //只要收到反向的包,就会设置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;
- …
例子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;
- }
看了上面的代码分析,我们得知对于可以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天不传输数据,然后反方向主动传输数据的情形也会存在。