1 Inside和Outside
很多在Cisco配置过NAT的人都有过一个疑问,那就是inside和outside的区别!以下是Cisco官方文档上关于NAT执行顺序的说明:
注意红色和蓝色圈住的部分,对于inside-outside而言,NAT发生在路由之后,而对于outside-inside而言,NAT发生在路由之前。这是目前为止,我们唯一需要记住的。
1.1 问题
迷惑的原因不在别的,就在inside,outside这个名字不好,实际上如果将inside-outside换成POST-ROUTING,将outside-inside换成PRE-ROUTING的话,就非常好理解了,最重要的是,换了名字之后,NAT看起来不再和设备的inside/outside网口域相关,而和“路由”发生了关系,虽然本质上没有任何变化。
后面会介绍,实际上,在理解Cisco的NAT的时候,根本不能将inside和outside单独拿出来理解,inside和outside仅仅是一个位置限定词,代表“某地”,而具体的是“到某地去”还是“从某地来”,还需要一个副词,这就是source和destination。在详述这个之前,姑且先将inside和outside单独拿出来使用。
接下来我来说明一下NAT和路由的关系是多么重要!考虑以下的数据流,我以“路由”这个动作为中心:
正向包:-->NAT point1-->路由-->NAT point2-->
返回包:<--NAT point1<--路由<--NAT point2<--
我们看一下在NAT point1和NAT point2上要做些什么动作才合理。首先我们先考虑转换正向包的源IP地址发生在NAT point2,那么对于返回包,目标地址转换就发生在NAT point2,返回包转换完成目标地址后,发生路由查询,数据包正常返回,没有任何问题。现在考虑正向包的源IP地址转换发生在NAT point1,那么按照将NAT钩子操作安装在数据流同一位置的原则,返回包的目标地址转换只能发生在NAT point1,此时已经经过了路由查询,路由查询是基于目标地址转换前的目标地址来的,也就是说这个路由结果并非真正的路由结果,真正要想将返回数据包送到目的地,必须基于转换后目标地址来查询路由表才可以,然而即便这个针对转换前目标的路由查询结果实际上是个假的结果,你也要必须把它映射成一个真的结果(这就是ip nat outside source中add-route参数要做的事情,下面的例子详述)。以下给出一个实例:
为了使得返回包能够到达1.1.1.2,必须将到达2.2.2.4的路由映射到E0那一侧才行,因此必须增加一条:
ip route 2.2.2.4 255.255.255.255 1.1.1.2
也就是说将到达目标地址转换前的下一跳设置成目标地址转换后的下一跳。这就是NAT和路由的联动关系。Cisco中如果你用ip nat outside source设置NAT的时候,就会碰到上面的这个问题,你可以手工添加一条反向包的地址转换前路由映射,也可以通过命令行后面加add-route参数解决,add-route参数会自动为你添加那条路由,实际上它做的就是:
- 根据NAT映射表查询目的地址B转换后的目的地址A;
- 查找目的地址A查询到达A的下一跳C;
- 将到达B的下一条设置到C。
我们看到在outside上,即PRE-ROUTING上做源地址转换确实会有问题,但是可以通过add-route参数进行路由重新映射来解决之。正常合理的情况就是在inside上,即POST-ROUTING上做源地址转换。事实上,我们虽然在做源地址转换,但是问题总是出在反向数据包的目标地址转换上,合理的情况就是要将目标地址转换放在路由之前进行,反推正向数据,那就是将源地址转换放在路由之后进行,如果不了解Cisco Domain概念的话,一定会认为:不要在outside上作源地址转换!
目前为止,我来总结一下。Cisco完全割裂了路由和NAT的关系,不像Linux那样DNAT永远在路由之前进行,不管是配置的DNAT,还是SNAT的反向DNAT,都在路由前,这样就可以针对转换后的目标地址做路由查询,如果这么理解是对的,你会惊讶,Cisco竟然可以在PREROUTING(outside)做SNAT(ip nat outside source),导致反向包在POSTROUTING做DNAT,但是事情不是这么简单!
Cisco不会由于犯错而设计出如此不合理又容易把人搞晕的NAT架构的,它这么设计必然有自己的理由,是什么理由呢?下面2,3小节给出了一些提示。
1.2 关于policy routing
我们知道,标准的IP路由是基于目标地址的,但是为了增加更多的策略,policy routing可以用源地址来影响路由查询结果。在这种意义上,源地址转换在路由之前就是必要的,然而这样就会导致反向的目标地址地址转换发生在路由之后!到底是:
- 为了policy routing将SNAT置于路由之前
- 为了不必add-route(虽然它确实不是什么问题,而且是自动的),将DNAT置于路由之前
需要一个权衡!然而Cisco不像Linux那样去对称设计那5个HOOK点,Cisco的方式就划分Domain的,即inside和outside
1.3 关于Domain和NAT domain
Cisco设备一般连接两种网络环境,一种是自己内部的,另一种是外部公共的,这就将接口分为了两个域,一个是内部域,即inside,另外一种为外部域,即outside!这种分法的名字叫得特别好,以至于它被用在了很多的领域,比如nat,然而一旦用在了nat方面,就让人糊涂了。因此我提议,对Cisco架构不是很理解的,请尝试用POST/PRE ROUTING来理解inside和outside。但是本小节想做的就是阐明使用inside和outside是合理的。
对于NAT来讲,转换的是IP地址,而IP地址可以分为Global地址以及Local地址,前者是公网可路由的地址,后者是私有地址。从inside域到outside域,需要将所有的Local地址转换为Global地址,一个首要原则就是,Global的地址在未经允许是不能出现在inside域和DMZ域(路由器可能没有)的!决策点就是路由!因此outside到inside方向的地址转换必然要在路由之前完成。这就要求inside到outside方向的地址转换必然要在路由之后完成。即:ip nat inside source必然发生在路由之后,而:ip nat outside source 必然发生在路由之前。
如果你定义了某个接口比如FE0/0为outside,那么需要在接口上使能ip nat outside,这样的话,从该接口进入的包就会在路由之前去查询NAT表,如果找到对应的表项,就会执行NAT。同理如果定义某接口比如FE0/1为inside,那么需要在接口上使能ip nat inside,这样的话,从该接口进入的包先执行路由查找,然后去查询NAT表,如果找到对应的表项,则执行NAT操作。
1.3.1 转换方向以及转换点-Cisco NAT的设计
这个小节涉及到了对Domain的使用以及如何解读ip nat inside | outside source | destination命令。本小节总结了Cisco NAT设计的终极理论。为了简单,我不再引入Cisco定义的那四种地址以及它们和源/目标IP地址,方向的关联,这些概念都是额外的概念,最容易使人跑偏而最终陷进去。
Cisco没有像Linux那样使用“pre/post路由”这么技术化的术语来定义NAT的行为,而是完全根据Domain来定义,所谓的Domain,即路由器两边一边属于inside,另一边属于outside。那么所有的NAT无外乎就以下4种类型:
- 从inside到outside时转换源地址
- 从inside到outside时转换目标地址
- 从outside到inside时转换源地址
- 从outside到inside时转换目标地址
其中1和4互相隐含,2和3互相隐含。到此为止,我们发现Cisco的NAT并没有像Linux那么简单,Linux实际上就定义了两种NAT,即:
- SNAT,源地址转换
- DNAT,目标地址转换
然后其它的约束都是设计的时候内置的:路由前执行DNAT,路由后执行SNAT,包含隐含规则。这就是Cisco和Linux的NAT设计的终极区别!它们侧重点不同,Cisco强调使用者的使用域,Linux强调技术本身的合理性(如何配置就需要发挥想象力了)。我们先看一下Linux的NAT设计基准是什么。Linux的NAT是全局生效的,没有“将NAT应用于接口”的说法,因此接口就成了一个match。因此管理员只需要写match/target就可以了。
对于Cisco而言,为了将4种NAT配置界面全部导出给工程师,需要一个前提操作,那就是定义inside接口和outside接口,即在哪个接口上应用inside nat,在哪个接口上应用outside nat。到此,所有的4种NAT都必须能和任意类型(inside/outside)的接口单独组合。这就打破了平衡点,变成了马鞍面,你无法找到一个点,在PRE ROUTING和POST ROUTING中完成一切,举例,如果接口E1使能inside nat,E0使能outside nat,说明E1是inside接口,E0是outside接口,那么我们考虑从inside到outside方向的两种转换,一种是转换源地址,另一种是转换目标地址,我们把它们放在一个位置还是放在一个“虚拟的平衡点”(不一定是routing)两边,即两个位置呢?我们看下面的两个图,实际上代表了两种约束:
这两个图展开后是个典型的马鞍面,原点就是路由,之所以要有个原点,是因为基于Domain的配置中,数据包从inside到outside或者反过来必然要经过一个点,从本文最上面的那幅Cisco网站上的图,我们看到基于Domain的NAT行为不仅仅是一个NAT,它需要和ACL匹配,加解密等操作联动,这些操作所依赖的IP地址和NAT发生了关联,因此基于Domain的NAT行为一定要位于路由行为的两边。由于Cisco是按照Domain即inside/outside来进行配置的(在接口上应用特定Domain的规则),因此必然是这种设计方式,而对于Linux,NAT是全局的,接口只是一个match而已,因此就完全按照路由的约束来设计。
现在,我们可以来总结一下ip nat inside | outside source | destination的含义了。我把这个命令公式化:
ip nat P H
其中H代表要做源转换还是要做目标转换,H还有一个更加隐蔽的含义,那就是它和P指名了数据的方向,也就是说:数据是以P为H的。举例,ip nat inside destination表示数据是以inside为目标的(来自outside),做目标地址转换;ip nat outside source表示数据是以outside为源发往inside的,做源地址转换。
1.4 Cisco的destination转换
但是,但是如何对目标地址进行转换,即将访问一个公共Global地址时,将其转到一个内部的Local地址,这就是目的地址转换,也叫地址映射,Cisco如何来做呢?实际上,很多Cisco设备的ISO版本不允许你像Linux那样无限制做DNAT,而仅仅允许映射特定的IP地址+TCP/UDP端口对或者全IP。这肯定是在outside上做目标地址转换了,在相反的方向就是在inside上做源地址转换,即:
ip nat inside source static tcp $local_ip $local_port $global_ip $global_port
注意,必须是static的NAT,这涉及到下一节要说的“如何安装NAT”,对于TCP负载均衡做的ip nat inside destination这种定制化的NAT则不在本文讨论范围内。
2 如何安装NAT
2.1 风格
Linux的NAT是基于5元组的,也就是NAT结果和一个流(conntrack)关联在一起,这种关联导致同属于一个五元组的一个流的所有数据包的NAT策略必须一致,对于这一硬性规定有点太强硬了,因此我在Linux上做了好几个补丁来弥补Linux的不足,当然也可以用RAWNAT。
对于Cisco,NAT不和一个流关联,除非是Stateful的。既然不和流关联,那么如何做呢?Cisco会在特定的时间将“一条NAT映射策略”安装到系统的inside NAT表或者outside NAT表中,对于从网口进入的数据包,会根据网口是inside还是outside去匹配inside NAT表或者outside NAT表中的NAT规则,仅此而已。
不管是inside NAT表还是outside NAT表,都各有两张,一张是SNAT表,另一张是DNAT表,NAT表的拍脑袋想出的数据结构可以是:
NAT table {
type:SNAT or DNAT
direction:inside or outside
nodes:local/global mapping
}
对于每一个数据包,都要用源IP地址去查询SNAT表,用目标IP地址去查询DNAT表。然而对于Linux而言,需要的仅仅是查询conntrack结构,然后取出第一个包查询时记录于此的nat结果。
2.2 静态NAT
静态NAT就是一个一对一的NAT映射,也就是一个Local IP地址和一个Global IP地址之间的转换。在配置生效的时候,NAT转换规则就会被添加到NAT表中。
2.2.1 Cisco方式
当在inside方向上添加了一条NAT:ip nat inside source static a b
,系统会将a->b的源地址转换加入到inside的SNAT表中,同时将b->a的目标地址转换加入到outside的DNAT表中。针对后面的所有数据包,不管是从内部发起的,还是从外部发起的,都会根据接口使能的是inside nat还是outside nat来查表匹配。
2.2.2 Linux方式
Linux基于conntrack,因此即使你使用iptables -t nat -A POSTROUTING -s a -j SNAT --to-source b也只针对匹配该策略的第一个数据包,Linux的NAT的转换一方a是作为一个match出现的,因此它严格匹配第一个包的源地址,故反方向的数据包不会匹配,因此Linux的NAT都是单向的。
2.3 动态NAT
动态NAT不在配置的时候规定转换后的地址,而在第一个有转换需求(由ACL来判断)的数据包到来的时候才确定它要转换成什么地址。因此,配置生效的时候,没有任何NAT规则会被加入到NAT表中。
2.3.1 Cisco方式
当在inside方向添加了一条动态NAT:
ip nat pool NAME ...
ip nat inside source list $acl pool NAME
系统不会添加任何NAT规则,只有当某一个包匹配到了acl,要引发NAT的时候,系统会动态(基于pool类型来计算)从pool中选一个要转换成的IP地址,将其添加入inside的SNAT表中,同时针对反方向的目标地址转换规则生成并加入outside的DNAT表中。
因此,Cisco动态的NAT是单向的,因此反向的数据包进入时不会匹配到acl,不会引发NAT规则的生成,也就不会匹配到任何NAT规则。
2.3.2 Linux方式
再次重申,Linux的nat中,待转换的IP地址是一个match,因此不管是一对一的转换还是一对多的转换,原理都是一样的。Linux并不区分静态转换和动态转换。在内核中,永远都不会出现所谓的NAT映射表,iptables添加的NAT规则不会生成映射,数据包进入匹配nat成功,也不会生成映射,nat结果仅仅存在于conntrack中作为tuple的一部分体现。
2.4 查询方式
2.4.1 Linux方式
Linux的nat查询对于第一个包是逐条匹配iptables nat表规则,对于后续的包,则转化为针对五元组的conntrack哈希查询。
2.4.2 Cisco方式
对于Cisco而言,不像Linux那样PREROUTING处仅仅发生DNAT,POSTROUTING处仅仅发生SNAT,而是不管在PREROUTING还是在POSTROUTING,均可能发生SNAT和DNAT,因此不管在哪个nat HOOK point,均要用数据包的源IP地址和目标IP地址分别来查询SNAT表和DNAT表。既然这么对称,那么就可以将这个查询过程抽象出来,使之成为一个独立的虚拟接口,是为NVI!
3 使用NVI虚拟接口
3.1 虚拟接口理念
这个就不说了。OpenVPN的tap,Cisco IPSec VPN的VTI,包括下文描述的NVI,都属于虚拟接口,并且所有的虚拟接口都可以通过路由的方式将数据包导入进去,至于在里面执行什么操作,这就是虚拟接口“虚拟”的地方,可以将数据包导出到字符设备,也可以加解密,当然也可以做NAT...
3.2 Linux 2.4的nat route
Linux 2.4的nat route依赖于policy routing。
3.3 Cisco的domainless NAT
Domainless就是说不再区分inside和outside,只是单纯地做NAT,这个在Cisco中实现得特别清爽,没有用所谓的平衡点,进而两个方向NAT的处理HOOK点也不再基于平衡点对称,所有的NAT操作全部在PREROUTING上做,然而Cisco并没有用增加配置,挂接HOOK的方式实现,而是用一个叫做NAT Virtual Interface的虚拟接口来实现,这样有什么好处呢?说实话,从界面上看不出来,但是从其实现角度,就可以通过路由的方式将带有ip nat enable配置的接口进来的包全部导入这个虚拟接口NVI0中。然后用数据包的源地址和目标地址分别查询SNAT表和DNAT表,根据结果进行NAT操作,随后进入真正的路由查询,整体流程如下图所示:
可见,不管方向,不管路由,只要数据包进入了一块带有ip nat enable配置的物理网卡,就会进行NAT匹配以及匹配成功后的操作,不管是SNAT和DNAT都在这里进行。这个实现虽然很豪放,但是却解决了所有问题,平衡点的问题不存在了,数据包在进入真正的路由查询前,NAT就已经完成了,在路由器看来,NAT操作被藏起来了,就好像数据包本来就是那个样子一样。
当然Domainless的NAT也不再和任何其它操作关联,ACL,VPN感兴趣流匹配,policy routing等都和NAT无关。Cisco Domainless的NAT设计和Linux的NAT设备区别更加明显,虽然Linux的NAT也是Domainless的,但是在设计上却和Cisco基于Domain的NAT很像,因为二者的NAT都要和其它的Filter操作联动,对于Linux,NAT行为需要和大量的Netfilter行为联动。
文章评论