如果想在Linux上配置NAT,那么大家众所一言的就是使用iptables的NAT表来配置,iptables提供了灵活丰富的配置来配置SNAT和DNAT,然而我们知道iptables的NAT依赖了ip_conntrack,也就是说,凡是一个命中了NAT表规则的流就会有一条连接追踪生成,由于ip_conntrack追踪了所有的数据包,因此当有大量连接经过了本地设备时,ip_conntrack空间将被撑满,这个在接入区特别容易重现,在骨干网反而不容易重现,然而骨干网却又几乎不可能使用Linux。
如果Linux支持无状态的NAT就好了,这样就不怕conntrack爆满了,也不会因为conntrack查找而深深影响效率。然而如此的无状态NAT也有自己的缺点,那就是你必须为返回流量显式配置NAT规则,这样配置规则就整整增加了一倍,毕竟是无状态的嘛,NAT工作在无状态的IP层,而IP层是单向的。iptables正是依赖了ip_conntrack才能自动的转换返回包的地址信息的,因为ip_conntrack根据五元素追踪了每一个IP流。
幸运的是,Linux一直都实现了无状态的NAT,只是其很少被人了解罢了。在Linux 2.4内核上,使用iproute2可以实现无状态NAT,只是在2.6内核上其实现被取消了,这是因为Linux的路由模块是基于目的地址hash或者trie tree的,然而对于同一目的地址而言,其作为负载均衡或者其它目的的多个表项却是线性查找的,NAT的实现就是采用多个这样的表项来完成的,这样在大量NAT规则存在的情况下,就会增加查找时间而影响效率,因此自2.6.24开始,Linux使用TC来实现无状态的NAT,虽然这样的实现有点变态,然而却很好的借助了TC固有的高效的查询匹配方式。
本文不准备详述Linux的TC机制,仅仅解析其配置NAT的方法。具体来讲,NAT分为SNAT和DNAT,如果应用在网络接口即网卡上而不是应用在协议栈的Netfilter链上的话,SNAT将绑定在egress方向,而DNAT将绑定在ingress方向。需要注意的是,由于基于TC的NAT是无状态的,因此当你配置了ingress的DNAT之后,必须在相反的egress方向显式配置SNAT,你的配置量因此将增加一倍。以下我们先来配置ingress上的DNAT规则,效果是将访问本机的一个网卡上的一个IP装换为该网卡上的另一个IP地址。
IP地址规划:eth2上的两个IP:192.168.40.249/24 12.12.12.5/24
和往常一样,首先建立一个规则队列,也就是qdisc:
tc qdisc add dev eth2 ingress handle ffff
然后建立一个filter:
tc filter add dev eth2 parent ffff: protocol ip prio 10 u32 match ip dst 192.168.40.249/32 action nat ingress 192.168.40.249/32 12.12.12.5
这儿不得不说一下这个filter,首先filter并不是对每一种qdisc都管用的,它只对特定的队列管用,比如HTB队列,而对某一些队列则没有作用,这个可以从man tc文档中得到具体详情。另外,我们这里仅仅基于IP地址进行了过滤,如果要涉及到协议以及其源或者目的端口进行过滤,那么请使用fmark,这个可以使用iptables的mangle表来完成,而该表是不依赖ip_conntrack的。最后,针对这个filter,唯一的解释那就是其action子语句,该子语句说的是在ingress方向上将匹配结果的目的地址从192.168.40.249修改为12.12.12.5。
接下来我们来配置反方向的egress规则:首先建立一个qdisc:
tc qdisc add dev eth2 root handle 10: htb
然而配置一个和上面类似的filter:
tc filter add dev eth2 parent 10: protocol ip prio 10 u32 match ip src 12.12.12.5/32 action nat egress 12.12.12.5/32 192.168.40.249
该语句说的是,在egress方向上,将源地址从12.12.12.5装换为192.168.40.249这个地址。可以看出,该egress规则和上述的ingress规则是相反的。
需要指出的是,使用TC配置的无状态NAT,它和iptables是完全不同的理念,它基于网卡接口以及其方向,而不是基于协议栈实现的N个HOOK点,因此使用TC配置的无状态NAT并没有本机发出包和Forward包之间的区别,一切都需要你自己在接口上显式的进行配置才可以。比如虽然在ingress上将目的地址配置成了12.12.12.5,TC并看不到这个地址到底是谁的,是本机的?还是其它网络的?TC并不知道,一切都由最终的路由来决定。反过来在配置egress的返回规则时,为何要在eth2这个设备上进行配置,这完全取决于你的路由,这是和iptables一致的,也就是说,SNAT必须在路由后完成,而DNAT则必须在路有前来完成。和iptables不同的是,TC配置的无状态NAT并没有区分OUTPUT和PREROUTING(这并不是一种模式,而是Linux协议栈的实现理念-一切都由callback来决定来决定的),而是统一于ingress和egress,这对于习惯于Cisco IOS或者VRP平台的家伙来讲,无疑更加习惯一些。
测试一下上述配置的正确性十分简单,但是千万别用tcpdump来抓包确认,因为tcpdump基于pcap,而它只是抓取混杂模式下的“未DNAT转换前的目的IP地址以及已经SNAT之后的源IP地址”,想了解这一点,那就看看Linux的PACKET_ALL的实现,它在数据从网卡读出还未做任何处理则进入了pcap,而对于外发包则是在数据马上就进入网卡的时候才进入pcap。
本文主要介绍了使用TC配置NAT的一般原理和步骤,如果习惯了这个配置,你可以就会抛弃一些被你抱怨已久的iptables nat方案了。