WWDC2015苹果宣布在iOS9支持纯IPv6的网络服务,并且要求2016年提交到AppStore的应用必须兼容纯IPv6的网络,要求适配的系统版本是iOS9以上(包括iOS9),否则会有过审被拒的可能。
到目前为止多个阿里云客户都已提前咨询阿里云是否支持IPv6,多个阿里云客户的App送审后审核不通过。在什么场景下会有问题?是否需要阿里云公网地址及相关服务立即支持IPV6?有什么过渡方式?这就是本文档需要回答的问题。
由于目前大部分服务器(包括阿里云)都没有Internet类型的IPv6地址,所以我们现在遇到的问题主要是App在IPv6-only网络下去访问IPv4-only网络的服务端,如果服务器需要兼容IPv6需要做很多软件和硬件上的全面升级。
幸好,从一开始设计IPv6就考虑到了向后兼容的问题,运营商会提供一个中间节点,使用DNS64/NAT64等技术,负责协议的转换和地址的转换,打通IPv6和IPv4之间的链路。这样我们在IPv6的环境下也是可以访问IPv4的后台资源的,我们的后台也就暂时不需要做什么变动。
对于使用域名方式访问服务端的情况,App Client端只需将网络层通信接口改造成兼容IPv6即可,App Client在IPv6-Only网络下会按照两种方式进行域名解析,如果域名配置了AAAA记录,则直接返回配置的IPv6地址,如果只有A记录,则DNS64会合成IPv6地址后返回给App Client,大部分非分区类型手机游戏和非游戏类App适合此场景。
对于使用IP方式访问服务端的情况,App Client端既需要将网络层通信接口改造成兼容IPv6,又需要实现IPv4和IPv6间的地址转换(App Client从服务器拉取的地址列表一般是IPv4类型的,连接服务端时需要用IPv6类型的IP,服务端返回包时不需要转换,因为NAT64服务已经自动做了转换),对于分区或分服类手机游戏适合此场景。
兼容IPv6的网络接口可以直接使用苹果官方提供的版本,逐一在App Client端代码中做替换即可。
IPv6是Internet Protocol Version 6的缩写,简单的概括IPv6就是现行的互联网协议(IPv4)的下一代IP协议。IPv6由128位二进制数组成,可提供庞大的IP地址资源,足以让地球上每个生物乃至每厘米都能分配到一个或多个IP地址。将这128位的地址按每16位划分为一个段,将每个段转换成十六进制数字,并用冒号隔开。
IPv4地址示例:192.168.1.1
IPv6地址示例:2001:0db8:85a3:08d3:1319:8a2e:0370:7344
目前互联网广泛应用的IPv4技术,理论上IPv4是一个32位的二进制数的地址,可编址1600万个网络、40亿台主机。但在采用了A、B、C三类编址方式后,可用的网络地址和主机地址数目大打折扣,欧美国家掌握着核心技术,且互联网发展较早,因此拥有约3/4的IP资源。造成我国及其他发展中国家的IP地址资源不足的困局,随着中国互联网用户的不断增加和电子、网络技术的蓬勃发展,缺乏IP地址资源,将严重制约我国及其他发展中国家互联网的应用和发展。
由于从IPv4网络完全过渡到IPv6网络需要全球互联网基础设施中的网络软件和网络硬件设备以及终端设备都支持IPv6协议,这会涉及到大量的改造工作,虽然得到各国政府和各大运营商的重视和推动,但是IPv4和IPv6仍将长期共存。
现在我们大部分服务器都是使用IPv4接入互联网的,我们要如何做兼容呢?也就是说如何做到IPv6和IPv4的兼容和相互访问?
要想使应用完全支持IPv6的环境,从协议到硬件,要做比较彻底的调整。不但客户端要做IPv6的改造,服务器也要适配IPv6,主要有以下四种对应关系,必须做好以下每一种:
IPv4->IPv4
IPv4->IPv6
IPv6->IPv4
IPv6->IPv6
要做到IPv6和IPv4完全兼容需要做很大的修改,最简单的协议上要兼容128位的IP地址,路由器、服务器等相关硬件也要升级。应苹果公司的要求,我们重点关注客户端从IPv6的网络环境访问IPv4的服务资源。
IPv6转换机制有很多种,苹果期望iOS App能够兼容DNS64/NAT64的方式。
(a) socket api支持RFC 4038—Application Aspects of IPv6 Transition
v4 socket接口只能支持IPv4 stack
v6 socket能支持IPv4 stack和IPv6 stack
(b) 服务器IP
返回v4 IP
返回v6 IP
(c) 用户本地IP stack
IPv4-only
IPv6-only
IPv4-IPv6 Dual stack
(d) 各种IPv6转换机制
NAT64/DNS64 64:ff9b::/96用于v6的本地网络通过NAT访问v4的资源:RFC6146、RFC6147。
6to4 2002::/16用于两个拥有v4公网地址的IPv6-only子网的互相访问:RFC6343。
Teredo tunneling 2001::/32 通过隧道的方式让两个IPv6-only子网互相访问,没有NAT问题:RFC4380。
464XLAT用于程序只有v4地址(使用v4 socket),但是本地网络是IPv6网络,程序需要访问v4资源,类似NAT64,不过区别在于服务器是运营商提供,手机上需要安装CLAT服务:RFC6877。
还有很多兼容方案,复杂程度都很高。幸好,从一开始设计IPv6就考虑到了向后兼容的问题,运营商会提供一个中间节点,使用DNS64/NAT64等技术,负责协议的转换,打通IPv6和IPv4之间的链路。(IPv6和IPv4互通技术有很多,这里只讨论Apple要求的技术方案DNS64/NAT64)。
这样我们在IPv6的环境下也是可以访问IPv4的后台资源的,我们的后台也就暂时不需要做什么变动。
在这样的情况下,我们虽然用的是v6的socket,但是必须要让socket走的是v4的协议。
::ffff:0:0/96 — This prefix is designated as an IPv4-mapped IPv6 address. With a few exceptions, this address type allows the transparent use of theTransport Layer protocols over IPv4 through the IPv6 networking application programming interface. Server applications only need to open a single listening socket to handle connections from clients using IPv6 or IPv4 protocols. IPv6 clients will be handled natively by default, and IPv4 clients appear as IPv6 clients at their IPv4-mapped IPv6 address. Transmission is handled similarly; established sockets may be used to transmit IPv4 or IPv6 datagram, based on the binding to an IPv6 address, or an IPv4-mapped address. (See also Transition mechanisms.)
从上文可以看到如果服务器地址为128.0.0.128,我们转换成IPv4-mapped IPv6 address::ffff:128.0.0.128或者纯16进制::ffff:ff00:00ff,然后赋值给sockaddr_in6.sin6_addr=”::ffff:128.0.0.128”;。这个socket虽然用了IPv6的sockaddr_in6,但实际上走的是IPv4 stack。
IPv4-mapped IPv6 address是让用户能够使用一致的socket api来访问IPv4和IPv6网络。
这里我们先看看Wikipedia对NAT64/DNS64的描述:
NAT64 is a mechanism to allow IPv6 hosts to communicate with IPv4 servers. The NAT64 server is the endpoint for at least one IPv4 address and an IPv6 network segment of 32-bits, e.g., 64:ff9b::/96 (RFC 6052, RFC 6146). The IPv6 client embeds the IPv4 address with which it wishes to communicate using these bits, and sends its packets to the resulting address. The NAT64 server then creates a NAT-mapping between the IPv6 and the IPv4 address, allowing them to communicate.
DNS64 describes a DNS server that when asked for a domain's AAAA records, but only finds A records, synthesizes the AAAA records from the A records. The first part of the synthesized IPv6 address points to an IPv6/IPv4 translator and the second part embeds the IPv4 address from the A record. The translator in question is usually a NAT64 server. The standard-track specification of DNS64 is in RFC 6147.[10]
There are two noticeable issues with this transition mechanism:
NAT64是一种有状态的网络地址与协议转换技术,一般只支持通过IPv6网络侧用户发起连接访问IPv4侧网络资源。但NAT64也支持通过手工配置静态映射关系,实现IPv4网络主动发起连接访问IPv6网络。NAT64可实现TCP、UDP、ICMP协议下的IPv6与IPv4网络地址和协议转换。
DNS64则主要是配合NAT64工作,主要是将DNS查询信息中的A记录(IPv4地址)合成到AAAA记录(IPv6地址)中,返回合成的AAAA记录给IPv6侧用户。NAT64一般与DNS64协同工作,而不需要在IPv6客户端或IPv4服务器端做任何修改。
这里大概描述一下NAT64的工作流程,首先局域网内有一个NAT64的路由设备并且有DNS64的服务。
a 客户端进行getaddrinfo的域名解析
b DNS返回结果,如果返回的IP里面只有v4地址,并且当前网络是IPv6-only网路,DNS64服务器会把v4地址加上64:ff9b::/96的前缀,例如 64:ff9b::14.17.32.211。如果当前网络是IPv4-only或IPv4-IPv6,DNS64不会做任何事情。
c 客户端拿到IPv6地址进行connect。
d 路由器发现地址的前缀为64:ff9b::/96,知道这个是NAT64的映射,是需要访问14.17.32.211。这个时候需要进行NAT64映射,因为到外网需要转换成IPv4 stack。
e 当数据返回的时候,按照NAT映射,IPv4回包重新加上前缀64:ff9b::/96,然后返回给客户端。
Apple的文档里面也有很详细的描述:
//NAT64 address sample
//address init
const char* ipv6_str = “64:ff9b::14.17.32.211”;
in6_addr ipv6_addr = {0};
int v6_r = inet_pton(AF_INET6, ipv6_str, &ipv6_addr);
sockaddr_in6 v6_addr = {0};
v6_addr.sin6_family = AF_INET6;
v6_addr.sin6_port = htons(80);
v6_addr.sin6_addr = ipv6_addr;
//socket connect
int v6_sock = socket(AF_INET6,SOCK_STREAM,IPPROTO_TCP);
std::string v6_error;
if (0 != connect(v6_sock, (sockaddr*)&v6_addr, 28))
{
v6_error = strerror(errno);
}
//get local ip
sockaddr_in6 v6_local_addr = {0};
socklen_t v6_local_addr_len = 28;
char v6_str_local_addr[64] = {0};
getpeername(v6_sock, (sockaddr*)&v6_local_addr, &v6_local_addr_len);
inet_ntop(v6_local_addr.sin_family, &v6_local_addr.sin6_addr, v6_str_local_addr, 64);
close(v6_sock);
举个例子:
这里讨论比较坑的地方,按照NAT64的规则,客户端如果没有做DNS域名解析的话,客户端就需要完成DNS64的工作。这里的关键点是,发现网络是IPv6-only的NAT64网络的情况下,我们可以自己补充上前缀64:ff9b::/96,然后进行正常的访问。
注:AAAA记录(AAAA Record)是用来将域名解析到IPv6地址的DNS记录,用户可以将一个域名解析到IPv6地址上,也可以将子域名解析到IPv6地址上。
这里一般connect的时候会返回错误码network is unreachable,因为根本没有v6的协议栈,就像没有硬件设备一样,但是不排除会有系统会返回no route to host。当然,如果服务器的地址是Teredo tunneling 2001::/32,可以客户端直接做隧道。如果是6to4 2002::/16,并且客户端有RAW socket权限加上非NAT网络,这种情况下可以客户端自己做6to4的路由。
这里只要没有配置上,是可以直接通讯的。当然这里会涉及到一个问题,如果DNS返回上文说的6to4或Teredo tunneling或pure native IPv6 address,这样的情况下我们怎么做IP的选择呢,可以参照RFC 3484 – Default Address Selection for Internet Protocol version 6(IPv6)。
整体原理如下:
iOS设备需要连接Mac机创建的NAT64/DNS64的Wi-Fi,就是传说中的IPv6的网络环境,再通过有线网络、路由器,访问到IPv4的资源,就可以做到IPv6->IPv4的连接。
需要一台用非Wi-Fi方式上网的Mac电脑(10.11以上的系统)、iOS9以上(包含iOS9)设备和互联网有线接口。目的是用Mac作一个热点,然后用iPhone连接这个Wi-Fi。我们产生的是一个本地的IPv6 DNS64/NAT64网络,这项功能是OS X 10.11新加的。
a 在“系统偏好设置”界面选中“共享”的同时,要按住“option”键。
b 不要松开“option”键,选择“Internet Sharing”;
c 松开“option”键,选择“Create NAT64 Network”选框;
d 选择提供网络连接的网络接口设备;
e 选择“Wi-Fi”选框;
f 点击“Wi-Fi Options”并配置网络名和安全选项;
g 选择“Internet Sharing”选框以生效本地网络;
h 点击“Start”开始网络共享;
现在可以用iPhone连接上这个刚创建好的热点开始测试了,注意此时要把iPhone设成飞行模式,以保证只用Wi-Fi上网。
测试重点:
a IPv4和IPv6网络环境判断是否正确
b UDP和TCP的切换是否正确。
下图展示的蓝色部分的这些API都是不存在兼容性问题的,而我们平时自己用的包括那些第三方的网络库大部分都是用的这些API:
大部分情况下,我们用高级的API完全能够实现我们的需求,而且高级API封装得很便于使用,很多底层的像适配IPv6的工作都已经完成,而用底层API会有大量的工作要我们自己来做,更容易产生BUG,如果确实需要用底层的POSIX Socket API,请参照RFC 4038:Application Aspects of IPv6 Transition的指导。
SCNetworkReachabilityCreateWithName 比如这个API的nodename参数不要传IP地址,而应该是域名;
这些API都是只针对IPv4做处理的,换用兼容IPv4及IPv6的API。
判断当前客户端是处于IPv4-only、IPv6-only还是IPv4和IPv6并存的环境,然后分别使用不同的网络API。
换用CoreFoundationframework及以上Apple提供的API,这样,即使我们填的是IPv4的地址,系统会在不同的网络环境自动帮我们进行地址的转换,我们不需要额外的工作(例如CFStreamCreatePairWithSocketToHost、NSURLSession)。
客户端做不同的处理的前提是需要知道客户端可用的IP协议栈。可用的IP stack类型分别是IPv4-only、IPv6-only、IPv4-IPv6 Dual stack。
我们先定义客户端可用的IP协议栈的含义:获取客户端当前能使用的IP协议栈。例如iOS在NAT64 Wi-Fi连接上的情况下,Mobile的网虽然存在IPv4的协议,但是系统是不允许使用的。iOS只能使用Wi-Fi的协议栈,在NAT64 Wi-Fi的情况下就是IPv6-only网络了。如果遇到IPv6-only网络,需要把它当作NAT64来处理,在v4 IP前添加前缀64:ff9b::/64。但是NAT64和IPv6-only不是等价的。IPv6-only网络可能支持NAT64,能访问v4的互联网资源,但是IPv6-only能访问v6的互联网资源,不支持NAT64。这里假设IPv6-only的网络都是支持NAT64的,对v4 IP进行64:ff9b::/96的处理。
这里的方案是直接做DNS解析,然后判断返回的IP有没有带上64:ff9b前缀来确定当前的IP协议栈。这也是唯一能够判断IPv6-only网络是否支持NAT64的方案。
//gateway
in6_addr addr6_gateway = {0};
if (0 != getdefaultgateway6(&addr6_gateway))
return EIPv4;
if (IN6_IS_ADDR_UNSPECIFIED(&addr6_gateway))
return EIPv4;
in_addr addr_gateway = {0};
if (0 != getdefaultgateway(&addr_gateway))
return EIPv6;
if (INADDR_NONE == addr_gateway.s_addr || INADDR_ANY == addr_gateway.s_addr)
return EIPv6;
//getaddrinfo
struct addrinfo hints, *res, *res0;
memset(*hints, 0, sizeof(hints));
hints.ai_family = PF_INET6;
(1) wikipedia Transition from IPv4 :leftwards_arrow_with_hook:
(2) wikipedia NAT64 :leftwards_arrow_with_hook:
(3) wikipedia DNS64 :leftwards_arrow_with_hook:
(4) wikipedia IPv6 address :leftwards_arrow_with_hook:
(5)https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html