2014年(9)
分类: LINUX
2014-02-28 22:16:03
网络地址转换:NAT
Netfitler为NAT在内核中维护了一张名为nat的表,用来处理所有和地址映射相关的操作。诸如filter、nat、mangle抑或raw这些在用户空间所认为的“表”的概念,在内核中有的是以模块的形式存在,如filter;有的是以子系统方式存在的,如nat,但它们都具有“表”的性质。因此,内核在处理它们时有很大一部操作都是相同的,例如表的初始化数据、表的注册、钩子函数的注册等等。关于NAT表的初始化模板数据和表的注册流程并不是本文的重点,大家可以对照第四篇博文中filter表的相关分析来研究。本文还是侧重于从整体上对整个NAT子系统的设计思想和原理进行,当然,有时间我还是会简单和大家分析一NAT表的东西。因为最近确实太忙了,本来想着在四月份结束这个系列,无奈一转眼就晃到了五月份,做IT的娃,都不容易啊!
通过前面的几篇文章我们已经知道,NAT的设计是独立于连接跟踪系统的,即连接跟踪是NAT的基础框架,我们还了解到连接跟踪不会修改数据包,它只是负责维护数据包和其所属的业务会话或数据连接状态的相关信息而已。连接跟踪最终是被iptables模块所使用的,它所定义的那些状态信息如NEW、ESTABLISHED、RELEATED等等,NAT统统不用关心。
根据前面的hook函数挂载图我们可以清晰的知道,对于那些需要被本机转发的数据包,注册在NF_IP_PRE_ROUTING点的ip_nat_in ()函数完成对其目的地址转换的操作,注册在NF_IP_POST_ROUTING点的ip_nat_out()函数完成源地址转换任务。如果在编译Linux内核源码时打开了CONFIG_IP_NF_NAT_LOCAL选项,则注册在NF_IP_LOCAL_OUT和NF_IP_LOCAL_IN点的hook函数就会工作,最常见的用法的是用NF_IP_LOCAL_OUT点的ip_nat_local_fn()函数来改变本机发出报文的目的地址。至于注册在NF_IP_LOCAL_IN点的ip_nat_fn()函数一般不会起作用,只是当数据包到达该HOOK点后会例行被调用一下。因为,NAT的所有规则只可能被配置到nat表的PREROUTING、POSTROUTING和OUTPUT三个点上,一般也很少有人去修改那些路由给本机的报文的源地址。
NAT的分类如下图所示:相信大家在看iptables用户指南都见过这么一句解释:
只有每条连接的第一个数据包才会经过nat表,而属于该连接的后续数据包会按照第一个数据包则会按照第一个报所执行的动作进行处理,不再经过nat表。Netfilter为什么要做这个限制?有什么好处?它又是如何实现的?我们在接下来的分析中,将一一和大家探讨这些问题。
在ip_nat_rule.c文件中定义了nat表的初始化数据模板nat_table,及相应的target实体:SNAT和DNAT,并将其挂在到全局xt[PF_INET].target链表中。关于NAT所注册的几个hook函数,其调用关系我们在前几篇博文中也见过:
因此,我们的核心就集中在ip_nat_in()上。也就是说,当我们弄明白了ip_nat_fn()函数,你就差不多已经掌握了nat的精髓。ip_nat_in()函数定义定在ip_nat_standalone.c文件里。连接跟踪作为NAT的基础,而建立在连接跟踪基础上的状态防火墙同样服务于NAT系统。
关于ip_nat_fn()函数我们还是先梳理整体流程,以便大家对其有一个宏观整体的把握,然后我们再来分析其实现细节。这里需要大家对连接跟踪的状态跃迁有一定了了解。从流程图可以看出,牵扯到的几个关键函数都土黄色标注出来了。ip_nat_setup_info()函数主要是完成对数据包的连接跟踪记录ip_conntrack对象中相应成员的修改和替换,而manip_pkt()中才是真正对skb里面源/目的地址,端口以及数据包的校验和字段修改的地方。
目的地址转换:DNAT
DNAT主要适用于将内部私有地址的服务发布到公网的情形。情形如下:服务器上架设了Web服务,其私有地址是B,代理防火墙服务器有一个公网地址A。想在需要通过A来访问B上的Web服务,此时就需要DNAT出马才行。根据前面的流程图,我们马上杀入内核。
当client通过Internet访问公网地址A时,通过配置在防火墙上的DNAT将其映射到了对于私网内服务器B的访问。接下来我们就分析一下当在client和server的交互过程中架设在防火墙上NAT是如何工作。
还是看一下hook函数在内核中的挂载分布图。
在PREROUTING点当一个skb被连接跟踪过后,那么skb->ctinfo和skb->nfct两个字段均被设置了值。在接下来的分析中,对那些梢枝末节的代码我们都将不予理睬。盗个图:
这里牵扯到一个变量ip_conntrack_untracked,之前我们见过,但是还没讨论过。该变量定义在ip_conntrack_core.c文件里,并在ip_conntrack_init()函数进行部分初始化:
atomic_set(&ip_conntrack_untracked.ct_general.use, 1); set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status); |
同时,在ip_nat_core.c文件里的ip_nat_init()函数中又有如下设置:
ip_conntrack_untracked.status |= IPS_NAT_DONE_MASK; |
该变量又是何意呢?我们知道iptables维护的其实是四张表,有一张raw不是很常用。该表以-300的优先级在PREROUTING和LOCAL_OUT点注册了ipt_hook函数,其优先级要高于连接跟踪。当每个数据包到达raw表时skb->nfct字段缺省都被设置成了ip_conntrack_untracked,所以当该skb还没被连接踪的话,其skb->nfct就一直是ip_conntrack_untracked。对于没有被连接跟踪处理过的skb是不能进行NAT的,因此遇到这种情况代码中直接返回ACCEPT。
从上面的流程图可以看出,无论是alloc_null_binding_confirmed()、alloc_null_binding()还是ip_nat_rule_find()函数其本质上最终都调用了ip_nat_setup_info()函数。
ip_nat_setup_info()函数:
该函数中主要完成了对连接跟踪记录ip_conntrack.status字段的设置,同时根据可能配置在nat表中的DNAT规则对连接跟踪记录里的响应tuple进行修改,最后将该ip_conntrack实例挂载到全局双向链表bysource里。
在连接跟踪系统里根据skb的源/目的IP分别已经构建生成初始tuple和响应tuple,我们通过一个简单的示意图来回顾一下其流程,并加深对ip_nat_setup_info()函数执行过程的理解。在图1中,根据skb的源、目的IP生成了其连接跟踪记录的初始和响应tuple;
在图2中,以初始tuple为输入数据,根据DNAT规则来修改将要被改变的地址。这里是当我们访问目的地址是A的公网地址时,DNAT规则将其改成对私网地址B的访问。然后,计算被DNAT之后的数据包新的响应。最后用新的响应tuple替换ip_conntrack实例中旧的响应tuple,因为数据包的目的地址已经被改变了,所以其响应tuple也必须跟着变。
在图3中,会根据初始tuple计算一个hash值出来,然后以ip_conntrack结构中的nat.info字段会被组织成一个双向链表,将其插入到全局链表bysource里。
最后,将ip_conntrack.status字段的IPS_DST_NAT和IPS_DST_NAT_DONE_BIT位均置为1。
这里必须明确一点:在ip_nat_setup_info()函数中仅仅是对ip_conntrack结构实例中相关字段进行了设置,并没有修改原始数据包skb里的源、目的IP或任何端口。
ip_nat_packet()函数的核心是调用manip_pkt()函数:
在manip_pkt()里主要完成对数据包skb结构中源/目的IP和源/目的端口的修改,并且修改了IP字段的校验和。从mainip_pkt()函数中返回就回到了ip_nat_in()函数中(节选):
ret = ip_nat_fn(hooknum, pskb, in, out, okfn); if (ret != NF_DROP && ret != NF_STOLEN&& daddr != (*pskb)->nh.iph->daddr) { dst_release((*pskb)->dst); (*pskb)->dst = NULL; } return ret; |
正常情况下返回值ret一般都为NF_ACCEPT,因此会执行if条件语句,清除skb原来的路由信息,然后在后面协议栈的ip_rcv_finish()函数中重新计算该数据包的路由。
在数据包即将离开NAT框架时,还有一个名为ip_nat_adjust()的函数。参见hook函数的挂载示意图。该函数主要是对那些执行了NAT的数据包的序列号进行适当的调整。如果调整出错,则丢弃该skb;否则,将skb继续向后传递,即将到达连接跟踪的出口ip_confirm()。至于,ip_confirm()函数的功能说明我们在连接跟踪章节已经深入讨论了,想不起来的童鞋可以回头复习一下连接跟踪的知识点。
前面我们仅分析了从client发出的第一个请求报文到server服务器时,防火墙的处理工作。紧接着我们顺着前面的思路继续分析,当server收到该数据包后回应时防火墙的处理情况。
server收到数据包时,该skb的源地址(记为X)从未变化,但目的地址被防火墙从A改成了B。server在响应这个请求时,它发出的回应报文目的地址是X,源地址是自己的私有地址B。大家注意到这个源、目的地址刚好匹配被DNAT之后的那个响应tuple。
当该回应报文到达防火墙后,首先是被连接跟踪系统处理。显而易见,在全局的连接跟踪表ip_conntrack_hash[]中肯定可以找到这个tuple所属的连接跟踪记录ip_conntrack实例。关于状态的变迁参见博文八。
然后,该回应报文到达NAT框架的ip_nat_in()函数,流程和前面一样,但处理方式肯定不同。我们还是先看一下截止到目前为止,这条连接跟踪结构图:直接跳到ip_nat_packet()函数里,当netlink通知机制将连接跟踪状态由NEW变为REPLY后,此时dir=1,那么根据初始tuple求出原来的响应tuple:源地址为A,目的之为X。此时,server的响应报文,源地址为私有网段B,目的地址为X。路由寻址是以目的地址为依据,防火墙上有直接到client的路由,所以响应报文是可以被client正确收到的。但是,但可是,蛋炒西红柿,对于UDP来说client收到这样的回复没有任何问题的,但是对于TCPU而言确实不行的。这就引出我们接下来将要讨论的SNAT。
未完,待续…