面对让人无语的ip_conntrack,我有一种说不出的感觉!自从接触它到现在,已经两年多了,其间我受到过它的恩惠,也被它蹂躏过,被它玩过,但是又不忍心舍弃它,因为我找不到更好的替代。工作中,学习中,用到了ip_conntrack的几乎所有特性,然而这些都不能拿来主义得使用,过程中多少有些美中不足,多少会留下一些遗憾,总结下来,我遇到的典型而非全部的问题如下所列: 1.不能马上生效NAT问题; 2.需要confirm才能用的问题; 3.conntrack cache面对save/restore mark问题 4.双向NAT问题 5.filter表DROP掉的流头包所属的流无法被conntrack ... 这些问题,最终让我”发明“出很多小技巧,以下是我的handle方案: 针对问题1.写出了平滑生效NAT的模块; 这个是我在在《Linux系统如何平滑生效NAT》http://blog.csdn.net/dog250/article/details/9394853中提出的,后续又进行了一些修正。 针对问题2.马上着手解决,本文目的 有时候,你可能希望使用conntrack工具监控新到的数据包,于是你写出了: conntrack -E -e NEW 但是即使有数据包进来,可能也没有任何事件结果输出,因为这些数据包被filter给DROP掉了,于是这个问题就是问题5。 针对问题3.”提出“依赖condition match和conntrack timeout的慢速匹配模式 我们不能保证总能从save/restore mark的优化中获益,一般而言,我们设置下面的经典规则: iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT iptables -t mangle $以下都是逐条的慢速匹配规则,匹配到就打mark 启用了ip_conntrack,任何数据包都要绑定到一个唯一的conntrack,由于ip_conntrack的保存是基于超时时间(UDP,ICMP而言)或者协议(TCP而言)的,因此就会导致在上述规则增加前的一个流的头包过去以后,新增加的依赖ip_conntrack的上述iptables规则不能生效,必须等到该conntrack过期或者协议关闭之后,下一次重新建立conntrack流时才能生效,这会引起很多莫名其妙的问题。通过一个condition match,当新iptables规则被添加时,将其置为0,关闭IPMARK带来的优化,强行让数据包匹配所有的策略。关闭时间设计为(max_timeout+5)秒,其中max_timeout秒为conntrack项过期的最长时间,5秒是一个误差修正值。为了使conntrack项的最长过期时间为max_timeout秒,需要对sysctl参数进行conntrack的timeout调整,且需要禁用掉conntrack的协议限制。最终,引入了两种策略匹配模式,在新规则添加时,会进入慢速匹配模式,所谓慢速匹配模式,就是不依赖ip_conntrack流状态的规则完全遍历匹配模式,反之快速匹配模式则是依赖ip_conntrack状态的匹配模式,直接从conntrack结构体中获取上次的匹配结果。 针对问题4.模仿Cisco风格写出了双向静态NAT模块 但是没有和conntrack关联,这是一种遗憾,以至于每个包都不得不去查找静态NAT哈希,其实有更好的方案,那就是和conntrack关联,新增一个static NAT的extension加入conntrack,无非就是对两个方向发起的流均查一次static NAT表,而不是查iptables设置的NAT rule表。 针对问题5.马上着手解决,本文目的 ip_conntrack有一个confirm逻辑,即当数据流的头包离开协议栈的时候,会被confirm,只有被confirm的conntrack才会加入到conntrack哈希,目前来讲,离开协议栈的地点有两个,第一个是被forward出去,即从一个网卡发出去,第二个是进入用户态,即被本地socket接收。而被DROP的包不会到达这两个点,所以就不会到达confirm点,进而永远都不会建立conntrack条目,也就是说,被filter DROP的数据流头包代表的整个流都无法使用conntrack的任何特性。此时你可能想到了用save/restore mark的方式,然而这也不行,因为没有被confirm,所以就根本就没有conntrack被加入哈希,试问,你能将mark save到哪里去呢? 然而这样做是不合理的,要知道,被DROP也是一种离开协议栈的方式啊!实际上也需要confirm的。如此一来,我就可以将”被DROP“这个事实,记录在conntrack的extension里面,然后在下一个包进来的时候,在PREROUTING中取出这个结果,直接DROP而不用再去匹配filter ruleset,提高了效率。到底应该怎么做呢?实际上并不需要做一个extension,仅仅在数据流头包被DROP的时候将其conntrack给confirm一下即可。余下的事情就可以交给IPMARK了,比如可以在mangle表和filter表设置以下的ruleset: mangle: iptables -t mangle -I PREROUTING -j CONNMARK --restore-mark iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT iptables -t mangle -A PREROUTING -m state --state ESTABLISHED -m condition --condition fastmatch -j ACCEPT ....数百条mangle规则,匹配则打mark iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j CONNMARK --save-mark filter: iptables -A FORWARD -m mark ! --mark 0 -j DROP iptables-A FORWARD -m state --state ESTABLISHED -m condition --condition fastmatch -j ACCEPT ...成百上千条filter规则,匹配则打mark; iptables -A FORWARD -m mark ! --mark 0 -j CONNMARK --save-mark iptables -A FORWARD -m mark ! --mark 0 -j DROP 如果说为了判断该数据包是否要DROP掉就去遍历匹配成百上千条规则,那就会大大影响效率,何不用conntrack的policy cache呢?遗憾的是,数据包被DROP导致一个流无法confirm,因此无法使用conntrack的特性,如果被DROP的数据包也能绑定到一个conntrack,那么上述的ruleset就能省大事儿了,需要做的仅仅是在DROP的时候confirm一下而已,代码修改非常简单,我一般喜欢抓住最小交集做最小的改动,需要修改的地方只有两个: a>第一处修改: $K/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c中增加一个notifier_block,本来我想新增一个HOOK点的,可是为了不把Netfilter搞乱,我还是用了体制外的一种方法,那就是notifier_block,因为我太喜欢Netfilter的设计了,多一点就多了,少一点就残了,所以我不对它的5个HOOK点进行任何拓展和想象,具体的修改方式如下: //初始化list BLOCKING_NOTIFIER_HEAD(conn_notify_list); EXPORT_SYMBOL(conn_notify_list); //定义notifier_block static struct notifier_block nf_filter_drop_notifier = { .notifier_call = nf_confirm_handler, }; //在nf_conntrack_l3proto_ipv4_init中注册这个notifier_block blocking_notifier_chain_register(&conn_notify_list, &nf_filter_drop_notifier);
b>第二处修改: $K/net/ipv4/netfilter/ip_tables.c的ipt_do_table的最后几行修改一下: #ifdef DEBUG_ALLOW_ALL return NF_ACCEPT; #else if (hotdrop) return NF_DROP; else return verdict; #endif
改为: #ifdef DEBUG_ALLOW_ALL return NF_ACCEPT; #else if (hotdrop || verdict == NF_DROP) { blocking_notifier_call_chain(&conn_notify_list, hook, skb); return NF_DROP; } else return verdict; #endif
最后给出nf_confirm_handler的实现: static int nf_confirm_handler(struct notifier_block *this, unsigned long hook, void *argv) { struct sk_buff *skb = (struct sk_buff *)argv; switch (hook) { case NF_INET_FORWARD: case NF_INET_LOCAL_IN: //本应该将in,out,skb等封装在一个struct里面传过来的,但是希望早点看到结果 //就省略了,反正就用到一个skb而已,其它的暂时不管了。 ipv4_confirm(hook, skb, NULL, NULL, NULL); break; default: break; } return NOTIFY_DONE; }
另外别忘了声明一下这个conn_notify_list,我是在#include <linux/netfilter_ipv4.h>中声明的: extern struct blocking_notifier_head conn_notify_list;
编译十分顺利,用起来效果也十分明显,在5000条iptables规则下,性能几乎没有任何损耗,而且也没有报任何错误,因为它简单,所以它好用。 结语:conntrack主导下的整体方案(和硬件接口) 我看过BSD的Netgraph,接口要比Netfilter的好用,于是我就想把Netfilter的HOOK函数的调用机制更改一下,不再用协议栈调用,而是通过event的方式来触发!在所有的HOOK的所有event中,都可以统一使用ip_conntrack,即一切策略均可以保存在conntrack,实现数据流头包的一次匹配,后续包的conntrack查找,策略提取,动作执行的序列化操作。 基本思想就是“一切均可缓存在conntrack”,其中可以缓存的类别包括: 转发策略:接受还是丢弃 路由策略:从哪个接口发出 NAT策略:如何实现地址转换 流控策略:和流控相关的配置 封装策略:VPN或者GRE 感兴趣流匹配策略:是否感兴趣流 以上的这些都可以仅仅针对一个流的流头进行匹配,然后将结果cache到conntrack,后续的包就不必再去匹配了,而是仅仅需要查找到对应的conntrack,取出conntrack中缓存的策略,直接使用,本文以上的DROP notifiler就是为实现这个目标完成的第一步。现在我们看看现有的Netfilter有什么问题,其实没有什么问题,一切都很好,唯一的问题是可能很多人都不知道如何从一个HOOK点直接调用另一个HOOK点的HOOK函数,人们认为只有协议栈才可以调用HOOK点,也就是说被PRE/POST/IN/OUT/FORWARD等字面意义迷惑了,而实际上,NF_HOOK是在任何地方都可以调用的,看看bridge的实现,bridge-call-iptables就是这么玩的,到处都是从一个HOOK点函数直接调用NF_HOOK的例子。 好了,有了这个前提,我所谓的event机制其实早就已经有模板了,无非就是在一个HOOK函数中去触发另一个HOOK的函数,所以改变的就是一个名字,一个HOOK函数就是一个event!下面是一个例子。我们可以仅仅针对一个流头进行路由查找,然后将dst_entry缓存在conntrack里面,后续的包只要对应到该conntrack,就可以取出dst_entry,此时就不必继续进入PREROUTING-ROUTING-FORWARDING...了,而是直接触发POSTROUTING的转发事件,直接转发,这样其实也就类似BSD的Netgraph了,只是名字叫法不一样而已。Netfilter其实早就把框架给搭好了,现在需要做的就是写几个HOOK函数而已,当然,并不一定非要和iptables关联,和procfs,sysfs等关联均可,无非就是一个内核态和用户态的沟通方式而已! 最后是一个硬件接口问题,我之所以考虑上面那些,就是为了提高效率,要说效率,再好的算法也比不上三流算法注入的硬件,以上的想法和硬件结合是最好的了,所有的查表,转发等全部由硬件实现。由于硬件不灵活,所以更是需要接口的灵活,可以互相调用构成一张图,这才能发挥硬件最大的作用。 所有的查询全部归为conntrack的查询,流头慢速匹配规则,然后缓存到conntrack,或者慢速匹配模式(见上述的condition match实现的慢速匹配)匹配非流头,然后缓存到conntrack,总是,二者是不见不散,类似Linux kernel的dirver和device之间的probe关系一样。
本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1268838