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

Linux系统NAT实现机制的升级改进

一点牢骚和希望

一直以来,一直对Linux的NAT很不满,也写过《Linux系统如何平滑生效NAT》系列文章中的patch进行修补,还写过一些类Cisco实现的patch,然而都效果不大好,暴雨的夜晚,长假的倒数第二晚,虽然没有10月7日晚雨量大,可是10月6日晚上到7日凌晨,上海嘉定那边的雨也可以堪称暴雨了。一直想看却一直没有时间看的《斯巴达克斯 第三季》终于看完了,雨越大越兴奋,可是巴拉巴西的《链接》也看完了,《罗马人的故事》最后一卷也看完了,《黑天鹅》还没有到货,剩下的只有写点代码了...于是半瓶竹叶青陪我到天蒙蒙亮,修改了几个内核代码文件,debug了几小时,小睡了两小时后,起床去买新鲜的肉类和蔬菜以及海鲜,因为长假最后一天要一起在家吃火锅。火锅很爽,外面大雨如注,屋里热气腾腾...就这样,雨一直下到第二天早上。
        10月7日第一天上班,我正常出门,可是到了公司已经12点多,在上地铁的时候,涉水到膝盖,转弯,突然发现暗黄色漂浮物,臭气迎面而来,素不相识的一行人为了安全走在了一起,我在头阵...听说是附近的厕所出问题了,污水粪便就从地下涌了上来...继续前行,停下,还是回头?如果只有我一人,我肯定回头了,然而后面有俩MM,还挺时尚漂亮,都说要过去,然后再洗,另外一位本来应西装笔挺的正装哥们儿由于裤子太合身无法挽起,执意也要先走过去再说...我如此邋遢的该如何,可想而知...真心不想趟这浑水啊!!...
        这么多和工作无关的琐事,我太罗嗦了。步入正题了!

alloc_null_binding的作用

Linux的NAT实现是基于ip_conntrack的,这句话已经不知道说了多少遍。一切均实现在Netflter的HOOK函数里面,其逻辑一点也不复杂,然而有意个小小的要点,那就是:即使没有匹配到任何的NAT规则的和NAT无关的数据流,也要针对其执行一个null_binding,所谓的null_binding就是用其原有的源IP地址和目标IP地址构造一个range,然后基于这个range做转换,这看似是一个无用的东西,其实还真的有用。
        用处在哪里呢?注意null_binding只是不改变IP地址,其端口可能要发生改变。为何要改变和NAT无关的数据流的端口呢?因为和NAT有关的数据流可能为了五元组的唯一性已经将和NAT无关的数据流的某个端口给占用了,这就影响了和NAT无关的数据流五元组的唯一性。由于ip_conntrack是不区分是否和NAT有关的,而NAT操作要改变五元组,为了整个conntrack的五元组都是唯一的,哪怕只有一个数据流执行了NAT,也可能占用了某个其它数据流的五元组要素,进而引发连锁反应,所以全部要执行唯一性检测和更新,alloc_null_binding就是为了做这个操作。

彻底消除流头匹配NAT的概念

要是没有深入研究过Linux的NAT,只是仅仅会配置它的话,也许你还真的不知道NAT规则只对一个流的第一个起作用,确切的说,是只针对一个流的ip_conntrack结构体刚刚建立还没有confirm的时候起作用,因为有时ip_conntrack结构体会过期。只要这样的包离开了协议栈,流就被confirm了,接下来的属于同一个流的其它数据包就直接使用上述那个包的保存在ip_conntrack结构体中的NAT结果了。
        正是由于这个特点,使得你无法中途添加NAT规则使之立马生效或者修改已有的NAT结果。这种有状态的特性带来了很多的问题。之前写过《Linux系统如何平滑生效NAT》系列文章,做过一些修正补丁。然而那些补丁的问题在于:它们还是基于流头匹配NAT规则的小修小补。我们知道,这种小修小补最终的结果就是不可维护,那么何不来一个颠覆,即,不再采用流头匹配NAT的原则,改为想什么时候匹配就什么匹配的原则。这其实是一种更高层次的颠覆,即流头匹配原则是新的匹配原则的一种特例。
       废除了流头匹配原则后,我决定把何时执行NAT的决定权留给应用程序,因此我决定注册一个sysctl变量,当其非0时执行NAT,不管是不是已经confirm了。

什么时候需要匹配NAT规则

既然说流头匹配原则不好,会带来问题(比如confirm的连接由于没有NAT而僵持在那里的问题),那么肯定要指出何时执行NAT匹配是必要的,这叫有破有立。在以下的情况下,执行NAT是必要的:
1.数据流连接时,由于还没有做NAT而导致久久连不上的情形。此时数据流的CT状态依然是NEW;
2.数据流已经成功连接,但是需要改变一下源地址(改变目标地址意味着重新连接一个新的服务)。此时的数据流的TC状态是ESTABLISHED;
3.数据流已经经过NAT连接,但NAT规则改变了。此时的数据流的TC状态是ESTABLISHED;

哪些情况不能执行NAT

并不是所有的以上情况都适合执行NAT匹配进而执行NAT,我们不光要考虑双向五元组标示的ip_conntrack本身,还要考虑协议本身的语义。我们看一下TCP协议,由于TCP严格根据五元组维持一个既有的连接,修改任何因子都意味着连接不复存在。因此:
1.对于TCP之类的有连接4层协议而言,只有NEW状态的数据流才能执行NAT,非NEW状态意味着已经收到目标的反馈,执行NAT没有意义;
2.一个流的其中一个数据包已经做好了NAT,并且NAT规则没有改变的情况,此时反向五元组已经被改了,没有必要每次都去匹配一遍NAT规则表;

能做和不能做

对于能做的事情,一般而言你不做也可以,就是你可以做也可以不做,但是对于不能做的事情,基本就是严禁了,如果你做了,就会带来严重的后果或者即使没有严重的后果也完全是无用功,世界就是这么的不对称,有时点到为止,总是功不抵过!因此对于以上两个小节,‘什么时候需要匹配NAT规则’中的一些点,我把控制权交给了应用程序,因此导出了一个sysctl接口,而对于‘哪些情况不能执行NAT’中的情形,则由内核来控制。

代码实现

以上的所有落实下来的话就是代码了,我没有将标准的patch贴到文章,因为那是打patch的时候给程序看的,如果让人看,一大堆的+++---的肯定很扰乱视线,因此我换了一种方式,即//////////////////////////包围的为我添加的代码段,/////////////########包围的为我修改的代码段。本小节的结构为:
{{文件名\n代码段\n总体说明},...}:

include/net/netfilter/nf_nat.h

//避开ip_conntrack_status枚举成员即可,然而13可谓一个重量级的数字
#define NF_FORCE_NAT_BIT 13
说明:增加了一个新的CT状态,用来指示是否要做NAT匹配。


include/net/netfilter/nf_conntrack_l4proto.h
struct nf_conntrack_l4proto
{
...
    int (*can_force_nat)(struct nf_conn *ct, struct sk_buff *skb);
...
}
说明:nf_conntrack_l4proto结构体增加了一个can_force_nat回调函数,将判断是否能重新执行NAT的决定权交给4层协议自己而不是在ip_conntrack以及nat逻辑中为之代劳。


net/netfilter/nf_conntrack_proto_tcp.c
//////////////////////////
static int nf_ct_can_force_nat(struct nf_conn *ct, struct sk_buff *skb)
{
        //没什么好说的...
        return 1;
}
//////////////////////////
...
struct nf_conntrack_l4proto nf_conntrack_l4proto_tcp4 __read_mostly =
{
...
////////////////////