日期:2014-05-16  浏览次数:20842 次

Linux路由表的抽象扩展应用于nf_conntrack

思想

标准IP路由查找的过程为我们提供了一个极好的“匹配-动作”的例程。即匹配到一个路由项,然后将数据包发给该路由项指示的下一跳。
如果我们把上面对IP路由查找的过程向上抽象一个层次,就会发现,其实它还可以有别的用。抽象后的表述为:以数据包的源地址或者目标地址为键值去查询一张表,查到结果项以后执行结果项指示的一个动作。一个结果项为:
struct result_node {
       uint32 network;
       uint32 netmask;
       void *action;
};
以上这个思想多亏了路由查找中的“最长前缀匹配”原则,该原则是隐式的,但是该原则保证了最精确的匹配。

需求

在我的特殊场景中,我希望一个网络段(一个大的网络,一个小一点的网络,一台主机)发出的数据流和一个字符串描述关联起来,该字符串可以是描述,可以是用户名,它甚至可以是别的任意什么东西...以往这种情况会被认为是不符合UNIX哲学的,并且在以往,内存使用方式太小气,内存太奢侈。但是现如今,不需要吝啬内存了,我们便可以在内核里面塞入任何可以塞入的东西,只要设计得当,让它符合UNIX哲学精神即可。
       总的来讲,我希望从一个到达的数据包上取出一个事先配置好的字符串。

为何Linux没有实现

实际上我的想法一开始就是错误的。正确性在于我知道我是错的。为何一直以来我一直在“修补”Linux内核协议栈以及Netfilter扩展的各种不良或者不完备的实现呢,比如“立即生效NAT”,比如双向静态NAT,比如不完备的conntrack confirm机制,不一而足。这些缺陷难道Linux内核以及Netfilter社区的那帮大牛们意识不到吗?绝对不是这样,因为他们遵循的是Worse is better原则,该原则的核心就是简单主宰一切,为了简单可以舍弃该舍弃的一切。
       而我的做法,即实现很多协议栈不包含的东西,看起来就是违背的就是Worse is better原则,我用Completeness(完备性)原则替换了Simplicity(简单性)原则,而这两个的原则的正确表述应该是:
Simplicity
The design must be simple, both in implementation and interface. It is more important for the implementation to be simple than the interface. Simplicity is the most important consideration in a design.
Completeness
The design must cover as many important situations as is practical. All reasonably expected cases should be covered. Completeness can be sacrificed in favor of any other quality. In fact, completeness must be sacrificed whenever implementation simplicity is jeopardized. Consistency can be sacrificed to achieve completeness if simplicity is retained; especially worthless is consistency of interface.
所以,Linux的社区开发人员严格遵循了该原则,没有引入“复杂且不常用”的机制。而我为何非要实现它们呢?我的想法是一切为了满足我的需求,依照Simplicity优先原则,我不得不这么做,因为我这么做是为了不引入更复杂的机制。

分析

既然是给数据流绑定一个字符串,很显然,扩展nf_conntrack是绝佳的选择,如何扩展它我在前文已经做了详述。接下来需要考虑的是如何设置规则,很显然,写一个INFO iptables模块是一个选择:
iptables -t ...-A .... -j INFO --set-info "aaaaaaaaaaaa"
INFO模块的思想是:从skb取出conntrack结构体,进而取出acct扩展(我总是喜欢拿account扩展开刀),然后把set-info参数指示的信息拷贝到acct扩展中。
但是,如果有10000个info信息需要设置,我就要建立10000条规则,每一个数据包都要在内核中顺序的遍历以上所有的规则,iptables规则太多(超过5000条)的话,确实会严重影响网络性能。当然你可以灵活安排规则的顺序以便使数据包快速结束一个链的匹配过程,比如以下这样:
iptables ... -m state --state ESTABLISHED -j ACCEPT  (设置在第一条,因为只有针对一个流的第一个NEW状态的数据包才会有set-info的必要)
.... -j INFO --set-info aaaaa
.... -j INFO --set-info bbbbb
.... -j INFO --set-info ccccc

这确实是一种技巧,玩过iptables的都知道,但是由于存在以下4个问题导致我不得不寻找其它的方案:
1.当INFO和INFO之外的其它target并不一致同意通过以上的方式跳出规则匹配链的时候,就需要安排另一条自定义链来解决。
2.由于上述第1点的频繁操作,最终会演变成针对iptables的编程,规则集本身会越来越复杂,像写得不好的C++代码那样,调整一条规则的顺序都将是极其困难的。
3.退一万步说话,对一个NEW状态的包,它真的就需要遍历规则(注意,iptables是遍历)匹配,我需要一种比O(n)高效的算法(nf-HiPAC在5000+条规则情况下会好很多)。
4.Netflter的nf-HiPAC项目可能是个创举,但是我没有时间详细研究它。
UNIX思想教导我们要将问题拆成互相独立的小问题,然后让它们相互配合去解决最终的大问题,但是相互配合本身有时会成为新的问题,需要付出巨大的管理成本。并不是每一类问题都可以拆成cat file|grep key那样的方式的。
       当我发现IP路由的查找过程其实就是一个“匹配-动作”的模式之后,我便改了“动作”的解释,将“发往下一跳”改为“取出info信息”,注意,只是取出info信息,并没有设置它到conntrack,为何不把这两件事一起做呢?因为这两件事之间的关系和cat file|grep key之间的关系很像。上一小节的4个问题,我是这么回答的:
1.使用路由查找模块是完全独立于iptables,和iptables没有半毛钱关系;
2.使用路由查找模块不需要和任何其它模块接口,你只需要调用以下接口:
int nf_route_table_search(const u_int32_t src, char *info, int len);
如果src找到,则将和该地址所在的最长前缀匹配的网络关联的info信息取出来,如果没有与之关联的,则返回非0
3.效率更不用说,完全就是Linux内核的hash路由查找算法,用了十几年了,跑在各种环境下。
4.虽然我没有时间去研究nf-HiPAC项目,但是我对路由查找算法却早已精通,一个思想是:永远使用自己最熟悉的技术。

再次扩展

话说可以使用路由查找模块快速匹配到一个数据包关联的一个“路由项”,那么该路由项里面仅仅存一个字符串信息是不是太浪费了呢?能不能把路由项携带的信息也搞成可扩展的呢?答案无疑是肯定的,因为我已经将“下一跳”修改成了字符串信息,接下来就是取消这个类型定义就可以了,在路由项里面加一个void *指针无疑是可取的,但是为了使内存更紧凑,加入一个0长度数组是更好的选择。
       在内核中,struct fib_node指示一个路由项,为了和标准的区