本文紧接着《 》,来看一下如何使用ip_conntrack来cache路由结果。 首先,我们看一下在启用了ip_conntrack的机器上,一个数据包从进入协议栈到发出去,要经历多少查询,然后就知道如果优化掉某些次的查询了。首先需要将一个skb绑定到一个conntrack结构,这就需要一个tuple的查询,此处我们抛开流头的NAT查询以及mangle/filter rule的查询,然后进入ROUTING逻辑,首先要查询一个路由cache(幸运的是,新内核禁掉了cache查询),然后如果没有找到,则查询policy table,这样总共需要3次比较大的查询,如果路由条目很多的话,这3次查询将会非常损耗效率。 既然conntrack为每一个数据包都绑定了一个流,那么就可以将需要查询的东西在查到结果后缓存在这个流结构里面,后续的同一流的包在查询到对应的流结构时,直接取出来使用之,这样的话所有的查询就只归结到conntrack哈希的查询了,并且这种查询可以十分简单的基于硬卡来实现,大大提高了效率。本文的实验仅仅缓存路由,实际上可以缓存的东西很多,正如《我和ip_conntrack不得不说的一些事》http://blog.csdn.net/dog250/article/details/9732185最后所述,很多的策略都可以被conntrack缓存。 那么,在哪个HOOK点来缓存呢?很简单,缓存结果在POST_ROUTING的最后confirm这个地方进行(仅仅针对forward包),而查询缓存结果在PRE_ROUTING的刚刚查询到conntrack结构的地方进行。于是我就修改了ipv4_confirm和ipv4_conntrack_in这两个函数。按照标准做法,不要在nf_conn结构体中增加字段,而是使用其extend机制,遗憾的是,...系统仅仅定义了:
enum nf_ct_ext_id { NF_CT_EXT_HELPER, NF_CT_EXT_NAT, NF_CT_EXT_ACCT, NF_CT_EXT_ECACHE, NF_CT_EXT_NUM, };这些个ID,并且写死在了nf_conntrack_extend.h中了,如果修改了就要全部重新编译,本来我想增加一个 NF_CT_EXT_ROUTE的,为了不重新编译,只是借用了NAT这个extend,实现效果即可。需要修改的文件只有一个 $K/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c:
//万恶的我为了偷梁换柱,redifine了nf_conn_nat struct nf_conn_nat { struct rtable *rth; }; static struct nf_ct_ext_type route_extend __read_mostly = { .len = sizeof(struct nf_conn_nat), .align = __alignof__(struct nf_conn_nat), .id = NF_CT_EXT_NAT, .flags = NF_CT_EXT_F_PREALLOC, }; //设置conntrack的rtable static void conn_dst_set(struct nf_conn *ct, struct rtable *dst) { struct nf_conn_nat *rt = nf_ct_ext_find(ct, NF_CT_EXT_NAT); if (rt == NULL) { rt = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC); if (rt == NULL) { return; } rt->rth = NULL; } #include另外,在conntrack被free的时候,一定记得把route extend同时也free掉,否则它们就真的成为游离内存了。 正如3.x的内核将路由cache禁用的理由一样,路由cache本来就应该属于conntrack这个层次,不管是针对五元组的conntrack还是SDN那样更加广义的N元组追踪,它们本质上都是为“转发”这个动作提供一种策略,然后基于这个策略对数据包进行分类,不管是Cisco的CEF还是各种基于Netfilter的硬卡,还是SDN的用户自定义流,都是这种思想的体现,因此3.x的内核并不是说路由cache不好,而是说它应该处在它本应该属于的地方。 但是我的这个第一版修改有以下几个问题: 1.没有notify机制。也就是说如果路由改变了,要更新conntrack里面缓存的路由,或者直接失效它,这个还没有实现;2.路由cache的timeout问题。因为conntrack中cache的路由同时也被cache到了路由缓存的list中,那么如果删除了呢?3.没地方show出来当前都cache了哪些路由在conntrack里面4.其实嘛,ct == &nf_conntrack_untracked也是可以cache路由的啊! 针对路由的conntrack缓存已经实现,针对ACCEPT or DROP的conntrack缓存也在我的上一版修改的基础上和IPMARK结合可以实现,想想看还有什么可以缓存的,在这一版修改后,剩下的就是设计一套硬卡的接口了,将Netfilter的HOOK实现在其中,于是这些硬卡就真正可以STOLEN软实现的数据包转发路径了。实际上,在实现上,如今的Netfilter已经很好,conntrack+IPMARK几乎可以完成所有事情,只是被DROP的流头无法创建conntrack,不过这个已经被我的第一版修改了,现在也没有问题了。在实现的建议上,建议不要增加新的HOOK点,最好用notifier_block的方式来进行事件传递。if (rt->rth == NULL || ((rt->rth != NULL) && rt->rth->u.dst.output == dst_discard)) { dst_use(&dst->u.dst, jiffies); rt->rth = dst; } } static void save_dst(struct sk_buff *skb, struct nf_conn *ct) { struct rtable *rth; rcu_read_lock_bh(); rth = skb_rtable(skb); if (rth != NULL) { conn_dst_set(ct, rth); } rcu_read_unlock_bh(); } static unsigned int ipv4_confirm(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct nf_conn *ct; enum ip_conntrack_info ctinfo; const struct nf_conn_help *help; const struct nf_conntrack_helper *helper; unsigned int ret; /* This is where we call the helper: as the packet goes out. */ ct = nf_ct_get(skb, &ctinfo); //仅仅针对FORWARD包进行路由cache,因此判断HOOKNUM和sock if (ct && hooknum == NF_INET_POST_ROUTING && skb->sk == NULL && ct != &nf_conntrack_untracked) { save_dst(skb, ct); } if (!ct || ctinfo == IP_CT_RELATED + IP_CT_IS_REPLY) goto out; .... } static unsigned int ipv4_conntrack_in(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { unsigned int ret = nf_conntrack_in(dev_net(in), PF_INET, hooknum, skb); //仅仅在PRE_ROUTING检查过路包 if (ret == NF_ACCEPT && hooknum == NF_INET_PRE_ROUTING) { enum ip_conntrack_info ctinfo; struct nf_conn *ct; struct rtable *rth; struct nf_conn_nat *rt; ct = nf_ct_get(skb, &ctinfo); if (!ct) { goto out; } rcu_read_lock_bh(); rt = nf_ct_ext_find(ct, NF_CT_EXT_NAT); if (rt == NULL) { rt = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC); if (rt == NULL) { rcu_read_unlock_bh(); goto out; } rt->rth = NULL; } if ((rth = rt->rth) == NULL) { rcu_read_unlock_bh(); goto out; } dst_use(&rth->u.dst, jiffies); //以下将conn的路由cache设置进skb,如此一来就不用ROUTING了 skb_dst_set(skb, dst_clone(&rth->u.dst)); rcu_read_unlock_bh(); //注意以下的被注释的代码,实际上放开这些注释的话,所实现的功能和不放开注释 //的效果是完全不同的!以下的注释可以实现HOOK点间的跳转,十分方便和硬卡进行 //接口,你可以在NF_HOOK那一行调用硬卡接口实现直接发送,然后返回NF_STOLEN // NF_HOOK(PF_INET, NF_INET_FORWARD, skb, skb->dev, rth->u.dst.dev, // rth->u.dst.input); // return NF_STOLEN; } out: return ret;; }