ip_conntrack不得不说的一些事

2019年6月13日 0 作者 oceansw

面对让人无语的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关系一样。