iOS版应用支持IPV6,就那点事儿

1,019 阅读11分钟

果然是苹果打个哈欠,iOS的行业内就得起一次风暴呀。自从5月初苹果明文规定所有开发者在6月1号以后提交新版本需要支持仅IPv6的网络,大家便开始热火朝天的研究如何支持IPV6,以及应用中哪些模块目前不支持IPV6。

为了更好的交流,特建了一个IPV6交流群(群号:805558511),希望能否相互交流沟通问题:

一,纯IPv6的支持是啥?

首先IPV6,是对IPV4地址空间的扩充。目前当我们用iOS设备连接上Wifi,4G,3G等网络时,设备被分配的地址均为IPV4地址,但是随着运营商和企业逐渐部署IPV6 DNS64 / NAT64网络之后,设备被分配的地址会变成IPV6的地址,而这些网络就是所谓的纯IPv6的网络,并且仍然可以通过此网络去获取IPV4地址提供的内容。客户端向服务器端请求域名解析,首先通过DNS64服务器查询IPv6的地址,如果查询不到,再向DNS服务器查询IPv4地址,通过DNS64服务器合成一个IPV6的地址,最终将一个IPV6的地址返回给客户端。如图所示:

NAT64,DNS64,ResolutionOfIPv4_2x.png

在Mac OS 10.11+的双网卡的Mac机器(以太网口+无线网卡),我们可以通过模拟构建这么一个本地IPv6 DNS64 / NAT64的网络环境去测试应用是否支持IPV6-Only网络,大概原理如下:

local_ipv6_dns64_nat64_network_2x.png

参考资料:

developer.apple.com/library/mac…

二,苹果如何审核支持纯IPv6的?

首先第一点:这里说的支持IPV6-Only网络,其实就是说让应用在IPv6 DNS64 / NAT64网络环境下仍然能够正常运行。但是考虑到我们目前的实际网络环境仍然是IPV4网络,所以应用需要能够同时保证IPV4和IPV6环境下的可用性。从这点来说,苹果不会去扫描IPV4的专有API来拒绝审核通过,因为IPV4的API和IPV6的API调用都会同时存在于代码中(不过为了减小审核被拒风险,建议将IPV4专有API通过IPV6的兼容API来替换)。

其次第二点: Apple官方声明iOS9开始向IPV6支持过渡,在iOS9.2 +支持通过getaddrInfo方法将IPV4地址合成IPV6地址(在iOS 9.2和OS X 10.11.2中为getaddrinfo添加了合成IPv6地址的能力)。其提供的可达性库在iOS8上的系统下,当从IPV4切换到IPV6网络,或者从IPV6网络切换到IPV4,是无法监控到网络状态的变化。也有一些开发者针对这些错误询问苹果公司的审核部门,给予的答复是只需要在苹果最新的系统上保证IPV6的兼容性即可。

最后第三点:。只要应用的主流程支持IPV6,通过苹果审核即可对于不支持IPV6的模块,考虑到我们现实IPV6网络的部署还需要一段时间,短时间内不会影响我们用户的使用。但随着4G网络IPV6的部署,这部分模块还是需要逐渐安排人力进行支持。

追加第四点:如果应用一直直接使用IPV4地址通过NSURLConenction或者NSURLSession进行网络请求(一般需要服务器允许,且客户端需要在报头中伪装主机);经测试,IPV6网络环境下,直接使用IPV4地址在iOS9及以上的系统仍然能够正常访问;在iOS8.4及以下不能正常访问;这一点苹果的解释和建议是这样的:

注意:在iOS 9和OS X 10.11及更高版本中,NSURLSession和CFNetwork在DNS64 / NAT64网络上运行的设备上本地自动合成IPv4文本中的IPv6地址。但是,您仍应该努力摆脱IP地址文字的代码。

三,应用如何支持纯IPv6的?

对于如何支持纯IPv6的,官方给出了如下几点标准:(这里就不对其进行解释了,大家看上面的参考链接即可)

1.UseHigh-LevelNetworkingFrameworks;2.Don’tUseIPAddressLiterals;3.Check Source CodeforIPv6 DNS64/NAT64 Incompatibilities;4.UseSystemAPIstoSynthesizeIPv6Addresses;

3.1 NSURLConnection是否支持IPV6?

官方的这句话让我们疑惑顿生:

*使用高级网络API,如NSURLSession和CFNetwork框架,你按名称连接,你不需要改变你的应用程序使用IPv6地址的任何东西 *

只说了NSURLSession和CFNetwork的API不需要改变,但是并没有提及到NSURLConnection。从上文的参考资料中,我们看到NSURLSession,NSURLConnection同属于Cocoa的url loading系统,可以猜测出NSURLConnection在ios9上是支持IPV6的。

应用里面的API网络请求,大家一般都会选择AFNetworking进行请求发送,由于历史原因,应用的代码基本上都深度引用了AFHTTPRequestOperation类,所以目前API网络请求均需要通过NSURLConnection的发送出去,所以必须确认NSURLConnection的是否支持IPV6。经过测试,NSURLConnection的在最新的iOS9系统上是支持IPV6的。

3.2 Cocoa的URL加载系统从iOS哪个版本开始支持IPV6?

目前我们的应用最低版本还需要支持iOS7,虽然苹果只要求最新版本支持IPV6-Only,但是出于对用户负责的态度,我们仍然需要搞清楚在低版本上URL加载系统的API是否支持IPV6。

(为了解决我,做一些实验)待续~~~

3.3可达性是否需要修改支持IPV6?

我们可以查到应用中大量使用了可达性进行网络状态判断,但是在里面却使用了IPV4的专用API。

在Pods:Reachability中AF_INETFiles:Reachability.mstructsockaddr_inFiles:Reachability.h,Reachability.m

可达性应该如何支持IPV6呢?

(1)目前Github的开源库Reachability的最新版本是3.2,苹果也出了一个支持IPV6的Reachability的官方样例,我们比较了一下源码,跟Github上的Reachability没有什么差异。

(2)我们通常都是通过一个0.0.0.0(ZeroAddress)去开启网络状态监控,经过我们测试,在iOS9以上的系统上IPV4和IPV6网络环境均能够正常使用;但是在iOS8上IPV4和IPV6相互切换的时候无法监控到网络状态的变化,可能是因为苹果在iOS8上还并没有对IPV6进行相关支持相关。(但是这仍然满足苹果要求在最新系统版本上支持IPV6的网络)。

(3)当大家都在要求Reachability添加对于IPV6的支持,其实苹果在iOS9以上对Zero地址进行了特别处理,官方发言是这样的:

reachabilityForInternetConnection:它监视地址0.0.0.0,

该可达性视为特殊令牌,使其实际

监视设备的一般路由状态,包括IPv4和IPv6。

  • (instancetype)reachabilityForInternetConnection {structsockaddr_in zeroAddress;bzero(&zeroAddress,sizeof(zeroAddress));zeroAddress.sin_len =sizeof(zeroAddress);zeroAddress.sin_family = AF_INET;return[selfreachabilityWithAddress: (conststructsockaddr *) &zeroAddress];}

综上所述,可达不需要做任何修改,在iOS9上就可以支持IPV6和IPV4,但是在iOS9以下会存在漏洞,但是苹果审核并不关心。

四,底层的套接字API如何同时支持IPV4和IPV6?

由于在应用中使用了网络诊断的组件,大量使用了底层的套接字API,所以对于IPV6支持,这块是个重头戏。如果你的应用中使用了长连接,其必然会使用底层套接字API,这一块也是需要支持IPV6的。对于Socket如何同时支持IPV4和IPV6,可以参考谷歌的开源库CocoaAsyncSocket。

下面我针对我们的开源网络诊断组件,说一下是如何同时支持IPV4和IPV6的。

开源地址:https://github.com/Lede-Inc/LDNetDiagnoService_IOS.git

这个网络诊断组件的主要功能如下:

本地网络环境的监测(本机IP +本地网关+本地DNS +域名解析);

通过TCP Connect监测到域名的连通性;

通过Ping监测到目标主机的连通耗时;

通过TRACEROUTE监测设备到目标主机中间每一个路由器节点的ICMP耗时;

4.1 IP地址从二进制到符号的转化

之前我们都是通过inet_ntoa()进行二进制到符号,这个API只能转化IPV4地址。而inet_ntop()能够兼容转化IPV4和IPV6地址。写了一个公用的in6_addr的转化方法如下:

//for IPV6+(NSString*)formatIPV6Address:(structin6_addr)ipv6Addr{NSStringaddress =nil;chardstStr[INET6_ADDRSTRLEN];charsrcStr[INET6_ADDRSTRLEN];memcpy(srcStr, &ipv6Addr,sizeof(structin6_addr));if(inet_ntop(AF_INET6, srcStr, dstStr, INET6_ADDRSTRLEN) !=NULL){address = [NSStringstringWithUTF8String:dstStr];}returnaddress;}//for IPV4+(NSString)formatIPV4Address:(structin_addr)ipv4Addr{NSString*address =nil;chardstStr[INET_ADDRSTRLEN];charsrcStr[INET_ADDRSTRLEN];memcpy(srcStr, &ipv4Addr,sizeof(structin_addr));if(inet_ntop(AF_INET, srcStr, dstStr, INET_ADDRSTRLEN) !=NULL){address = [NSStringstringWithUTF8String:dstStr];}returnaddress;}

4.2本机IP获取支持IPV6

相当于我们在终端中输入的ifconfig命令获取字符串,然后对的ifconfig结果字符串进行解析,获取其中EN0(WiFi版),pdp_ip0(移动网络)的IP地址。

注意:

(1)在模拟器和真机上都会出现以FE80开头的IPV6单播地址影响我们判断,所以在这里进行特殊的处理(当第一次遇到不是单播地址的IP地址即为本机IP地址)。

(2)在IPV6环境下,真机测试的时候,第一个出现的是一个IPV4地址,所以在IPV4条件下第一次遇到单播地址不退出。

  • (NSString *)deviceIPAdress{while(temp_addr !=NULL) {NSLog(@"ifa_name===%@",[NSString stringWithUTF8String:temp_addr->ifa_name]);// Check if interface is en0 which is the wifi connection on the iPhoneif([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"] || [[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"pdp_ip0"]){//如果是IPV4地址,直接转化if(temp_addr->ifa_addr->sa_family == AF_INET){// Get NSString from C Stringaddress = [selfformatIPV4Address:((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr];}//如果是IPV6地址elseif(temp_addr->ifa_addr->sa_family == AF_INET6){address = [selfformatIPV6Address:((struct sockaddr_in6 *)temp_addr->ifa_addr)->sin6_addr];if(address && ![address isEqualToString:@""] && ![address.uppercaseString hasPrefix:@"FE80"])break;}}temp_addr = temp_addr->ifa_next;}}}

4.3设备网关地址获取获取支持IPV6

其实是在IPV4获取网关地址的源码的基础上进行了修改,初开把AF_INET-> AF_INET6,sockaddr - > sockaddr_in6之外,还需要注意如下修改,就是拷贝的地址字节数。去掉了ROUNDUP的处理(解析出来的地址老是少了4个字节,结果是偏移量搞错了,纠结了半天),具体参考源码库。

/* net.route.0.inet.flags.gateway */intmib[] = {CTL_NET, PF_ROUTE,0, AF_INET6, NET_RT_FLAGS, RTF_GATEWAY};if(sysctl(mib,sizeof(mib) /sizeof(int), buf, &l,0,0) <0) {address =@"192.168.0.1";}....//for IPV4for(i =0; i < RTAX_MAX; i++) {if(rt->rtm_addrs & (1<< i)) {sa_tab[i] = sa;sa = (structsockaddr )((char)sa + ROUNDUP(sa->sa_len));}else{sa_tab[i] =NULL;}}//for IPV6for(i =0; i < RTAX_MAX; i++) {if(rt->rtm_addrs & (1<< i)) {sa_tab[i] = sa;sa = (structsockaddr_in6 )((char)sa + sa->sin6_len);}else{sa_tab[i] =NULL;}}

4.4设备DNS地址获取支持IPV6

IPV4时只需要通过res_ninit进行初始化就可以获取,但是在IPV6环境下需要通过res_getservers()接口才能获取。

+(NSArray*)outPutDNSServers{res_state res = malloc(sizeof(struct__res_state));intresult = res_ninit(res);NSMutableArray*servers = [[NSMutableArrayalloc] init];if(result ==0) {unionres_9_sockaddr_union addr_union = malloc(res->nscount sizeof(unionres_9_sockaddr_union));res_getservers(res, addr_union, res->nscount);for(inti =0; i < res->nscount; i++) {if(addr_union[i].sin.sin_family == AF_INET) {charip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &(addr_union[i].sin.sin_addr), ip, INET_ADDRSTRLEN);NSStringdnsIP = [NSStringstringWithUTF8String:ip];[servers addObject:dnsIP];NSLog(@"IPv4 DNS IP: %@", dnsIP);}elseif(addr_union[i].sin6.sin6_family == AF_INET6) {charip[INET6_ADDRSTRLEN];inet_ntop(AF_INET6, &(addr_union[i].sin6.sin6_addr), ip, INET6_ADDRSTRLEN);NSStringdnsIP = [NSStringstringWithUTF8String:ip];[servers addObject:dnsIP];NSLog(@"IPv6 DNS IP: %@", dnsIP);}else{NSLog(@"Undefined family.");}}}res_nclose(res);free(res);return[NSArrayarrayWithArray:servers];}

4.4域名DNS地址获取支持IPV6

在IPV4网络下我们通过的gethostname获取,而在IPV6环境下,通过新的gethostbyname2函数获取。

//ipv4phot = gethostbyname(hostN);//ipv6phot = gethostbyname2(hostN, AF_INET6);

4.5 ping方案支持IPV6

Apple的官方提供了最新的支持IPV6的ping方案,参考地址如下:https:

//developer.apple.com/library/mac/samplecode/SimplePing/Introduction/Intro.html

只是需要注意的是:

(1)返回的数据包去掉了IPHeader部分,IPV6的头部分也不返回TTL(生存时间)字段;

(2)IPV6的ICMP报文不进行checkSum的处理;

4.6 traceRoute方案支持IPV6

其实是通过创建socket套接字模拟ICMP报文的发送,以计算耗时;

两个关键的地方需要注意:

(1)IPV6中去掉IP_TTL字段,改用跳数IPV6_UNICAST_HOPS来表示;

(2)sendto方法可以兼容支持IPV4和IPV6,但是需要最后一个参数,制定目标IP地址的大小;因为前一个参数只是指明了IP地址的开始地址。千万不要用统一的sizeof(struct sockaddr),因为sockaddr_in和sockaddr都是16个字节,两者可以通用,但是sockaddr_in6的数据结构是28个字节,如果不显式指定,sendto方法就会一直返回-1,erroNo报22无效的参数的错误。

关键代码如下:(完整代码参考开源组件)

//构造通用的IP地址结构stuck sockaddrNSStringipAddr0 = [serverDNSs objectAtIndex:0];//设置server主机的套接口地址NSDataaddrData =nil;BOOLisIPV6 =NO;if([ipAddr0 rangeOfString:@":"].location ==NSNotFound) {isIPV6 =NO;structsockaddr_in nativeAddr4;memset(&nativeAddr4,0,sizeof(nativeAddr4));nativeAddr4.sin_len =sizeof(nativeAddr4);nativeAddr4.sin_family = AF_INET;nativeAddr4.sin_port = htons(udpPort);inet_pton(AF_INET, ipAddr0.UTF8String, &nativeAddr4.sin_addr.s_addr);addrData = [NSDatadataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];}else{isIPV6 =YES;structsockaddr_in6 nativeAddr6;memset(&nativeAddr6,0,sizeof(nativeAddr6));nativeAddr6.sin6_len =sizeof(nativeAddr6);nativeAddr6.sin6_family = AF_INET6;nativeAddr6.sin6_port = htons(udpPort);inet_pton(AF_INET6, ipAddr0.UTF8String, &nativeAddr6.sin6_addr);addrData = [NSDatadataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];}structsockaddr *destination;destination = (structsockaddr *)[addrData bytes];//创建socketif((recv_sock = socket(destination->sa_family, SOCK_DGRAM, isIPV6?IPPROTO_ICMPV6:IPPROTO_ICMP)) <0)if((send_sock = socket(destination->sa_family, SOCK_DGRAM,0)) <0)//设置sender 套接字的ttlif((isIPV6?setsockopt(send_sock,IPPROTO_IPV6, IPV6_UNICAST_HOPS, &ttl,sizeof(ttl)):setsockopt(send_sock, IPPROTO_IP, IP_TTL, &ttl,sizeof(ttl))) <0)//发送成功返回值等于发送消息的长度ssize_t sentLen = sendto(send_sock, cmsg,sizeof(cmsg),0,(structsockaddr *)destination,isIPV6?sizeof(structsockaddr_in6):sizeof(structsockaddr_in));

本文为第三方转载,原文链接:www.jianshu.com/p/a6bab07c4…