1.引
IPv6的标准中不建议使用NAT,个中缘由何在?这是一个问题,正如我很早之前解释的那样,IPv4的NAT打破了互联网本身的“互联”特性,使得一部分IP地址不再双向可达,NAT为无方向的IP协议增加了一个方向,特别是stateful的NAT类型。然而IPv4的NAT旨在节约IP地址,而非所谓的增加IP的方向性以及隐藏私有IP,这些只是一种难以摆脱的副作用罢了。
IPv6的时代已经到来,只要不扩展到外太空,地球上的蚂蚁都可以使用IP设备了。IPv4的一些修补手段将不再需要,为了保持协议本身以及相关标准的纯洁性,IPv6几乎不再提起NAT。虽然不再提起,并不是说“你已不是你”,实现IPv6的NAT还是可能的,并且在某些情形下还是必要的。
2.相关的RFC
RFC6296的标题是
IPv6-to-IPv6 Network Prefix Translation,描述了IPv6下的NAT的实现要点,给出了一个合理的建议,既保持了IP的无方向性,又可以满足NAT的语义,这就是IPv6之NAT stateless的缘由,你不能再指望像IPv4的NAT那样只需要配置一条rule,然后反方向的rule动态生成,IPv6情况下,两个方向的rule都需要你自己手工来配置。
3.IPv6子网和NAT的关系
IP地址可以划分为几个段,包括网络前缀,子网标识,主机标识,这在IPv4和IPv6中没有什么不同。IPv4的NAT为了节约IP地址,也就是说,可供映射的IP地址pool中的地址小于或者远远小于其内部主机的数量,因此很有可能多个内部主机被映射成了同一个外部IP地址,这如何来区分它们,因此不得不引入诸如第四层协议,端口等信息了,也就是我们熟知的五元组信息,因此IPv4的NAT实现大多数都是基于五元组流的,这样就保证了内核保持的NAT信息项的唯一性,同时也引入了很多副作用。
IPv6地址持有将近128位可随意调配的位,鉴于地址空间的庞大,一般的单位都会被分配到一个拥有很大量地址的网段,此网段拥有足够多的地址来和内网主机进行一一映射,也就是说可用于映射的IP地址pool容量巨大无比,关键是这个一一映射如何来保持,既然不想再使用非IP层的信息来保持信息,那就要用纯IP层的信息了,这样对上层影响最小。对于IPv4,经典NAT使用了五元组来保持流标识信息,而对于IPv6,则更加绝妙,它利用(而不是使用)了checksum的算法,丝毫不管这个checksum是谁的checksum,因为它根本就不改变数据包的checksum...
4.IPv6和链路层的关系
IP标识一台主机,而MAC标识一个网络接口,它们其实是一对多的关系,然而现实中,一个IP往往会和一块网卡相关联,其中一个缘由就是可以自动生成链路层的路由信息。这是很方便的,然而对于IPv4,地址是自己配置或者通过DHCP来分配的,管理过程十分复杂,一不小心就会有地址冲突,不得不依靠ARP广播/RARP等技术来检测,之所以出现这种状况,一则因为分层理念太教条,谁说IP和链路层就不能有关联,谁说IP标示主机就不能关联网卡;二则因为IPv4地址空间太小,而MAC地址长度一般都会超过IP地址,虽然满足了RFC标准上的一对多的关系,但是使用起来很不方便。
IPv6解决了这个问题,使得地址冲突问题减缓以致几乎消失。我们知道,MAC地址标示了一块物理设备,是看得见摸得着的,有时你摸一下还会碰到静电,MAC地址和你的身份证号码一样不会重复,IPv6将MAC地址映射到IPv6地址解决了IP地址冲突的大问题,同时这对于NAT也是有益的,这会影响到地址的自动转换,每一个不会重复的内部地址转换为一个不会重复的外部地址,这一切都要归功于IPv6的地址空间的巨大,可以随意杂耍。
5.checksum无关性和自动转换
这个很好解释,小学毕业就应该能理解。考虑
a+b+c+d=X
其中X就是checksum,我们把a,b当成源IP地址的两部分,c,d当成目的IP地址的两部分,我们作源地址转换,将a和b都改变,比如a改变成了A,试问将b改成多少才能保持checksum的值X不变,这其实很简单,就是一个简单的一元一次方程求解的问题。IPv6的建议NAT实现也是这个原理,只不过上面的一元一次方程是实数域的,而这个是计算机布尔数域。既然可以不触动第四层的checksum值,那么NAT对第四层协议的影响也就减小了,虽然它还是解决不了诸如ESP/AH等穿越NAT的问题。
基于以上算法,IPv6在做NAT的时候,在给定的子网网段内,可以自动生成一个新的IP地址供映射之用,从算法本身来看,冲突的可能性非常之小致于0,上述的做法对于IPv4几乎是不可能的,因为IPv4地址空间太小了,每个单位保有的地址池容量也有限,你不能指望一个算法为你生成一个IP地址,因为这种生成的地址要不根本就不属于自己,地址冲突,要么就是重复,和其它的映射冲突。
既然IPv6的NAT机制“自动”为一个连接选择了一个IP地址,那么当返回包到来的时候,如何来把地址转换回原来的呢?我们知道,IPv6的NAT已经不再使用五元组来维护NAT映射信息,也不在内核维护这种信息,那么“转换回去”这件事就要完全靠算法本身了,恰恰就是算法本身能将转换后的地址再转回原来的,其依据就是本小节最开始处给出的一元一次方程解的唯一性,在IPv6的NAT实现中,算法只针对IP地址中16位的地址信息进行自动生成,而其它的则需要手工显式配置,由于内网IPv6地址可以使用MAC地址映射成唯一的地址,由于一元一次方程解的唯一性,那么转换后的地址也是唯一的,将这一切反过来,最后还是能映射回原始的IP地址的。
如果抛开地址转换这一说,仅仅考虑算法本身,那还是可以给出一个实际可以运行的代码的,该代码使用了计算checksum的算法:
#include <stdio.h> #include <stdlib.h> #include <string.h> //以下2个函数就是计算校验码的,具体的原理请参见RFC1071/RFC1624/RFC1141 static inline u_int16_t add16( u_int16_t a, u_int16_t b) { a += b; return a + (a < b); } static inline u_int16_t csum16(const u_int16_t *buf, int len) { u_int16_t csum = 0; while(len--) csum = add16(csum, *buf++); return csum; } int main(int argc, char **argv) { u_int16_t buf[18] = {0}; int i = 0; memcpy(buf, "efghhijk", 8); memcpy(buf+4, "12345678", 8); memcpy(buf+8, "xxyywert", 8); memcpy(buf+12, "zxcvkljh", 8); //正确做法是打印16进制数据,此处为了简单,打印了字符串 printf("原始数据:%s 长度:%d\n", (char*)buf, strlen((char*)buf)); printf("原始数据的校验码:%X\n", csum16(buf, 16)); u_int16_t tip[3] = {0}; memcpy(tip, "#$!%", 4); u_int16_t tip_sum = csum16(tip, 2); printf("\nNAT规则: efghhijk1234/12 -〉efghhijk#$!%/12\n\n"); printf("固定从第9个字节开始修改4个字节为:%s 其校验码为:%X\n", (char*)tip, tip_sum); //定位固定修改后的动态修改的初始地址,注意,我们仅仅修改16位信息 u_int16_t* pcsum = buf + 4+2; //计算动态修改的值 *pcsum = ~add16( add16( ~(*pcsum), ~csum16(buf+4, 2) ), tip_sum ); printf("动态修改的值为:%X\n", *pcsum); memcpy(buf+4, tip, 4); //完成修改 printf("当前数据:%s 长度:%d\n", buf, strlen((char*)buf)); printf("当前校验码:%X\n", csum16(buf, 16)); printf("-------------以下是还原操作-------------\n"); printf("\n反向NAT规则: efghhijk#$!%/12 -〉efghhijk1234/12\n\n"); u_int16_t tip2[3] = {0}; memcpy(tip2, "1234", 4); printf("我们只需要记住原始数据被固定修改前的:%s\n", tip2); u_int16_t tip_sum2 = csum16(tip2, 2); u_int16_t* pcsum2 = buf+6; *pcsum2 = ~add16( add16( ~(*pcsum2), ~csum16(buf+4, 2) ), tip_sum2 ); //还原 memcpy(buf+4, tip2, 4); printf("原始数据:%s\n", (char *)buf); printf("原始校验码:%X\n", csum16(buf, 16)); }
运行结果如下:
结果斐然。把以上的原理套用在IPv6的NAT上,就是一种实现。
6.Linux上的MAP66
如果有一个点子,总会有人实现它,何况如果不仅仅是一个点子而是一个标准,那么实现它就更是情理之中了。Linux是一个大熔炉,更是一个实验场,一些草案的最新进展总是会体现了Linux上,IPv6的NAT也不例外。MAP66是一个基本遵循RFC6296建议的Linux实现,编译安装很简单,详见其README,想试一下吗?很简单,和IPv4的iptables一样:
1.配置正向的转换规则,将源地址fdca:ffee:babe::/64网段的地址转换为2008:db8:1::/64网段的地址
ip6tables -t mangle -A POSTROUTING -s fdca:ffee:babe::/64 -o eth2 -j MAP66--src-to 2008:db8:1::/64
可以看出,没有显式指定任何具体地址,类似IPv4的MASQUERADE和IPv4的IP Pool,原理详见第5节。
2.配置反向包的转换规则,将正向包被转换过的地址再转换回去
ip6tables -t mangle -A PREROUTING -d 2008:db8:1::/64 -i eth2 -j MAP66 --dst-to fdca:ffee:babe::/64
可以看出,也没有显式指定任何具体的地址,更值得一提的是,内核并不维护任何关于NAT映射的信息,因此MAP66也不再依赖ip(6)_conntrack。
如果使能上述两条规则,ping一下对端地址,tcpdump抓包的结果如下: