iOS 应用支持 IPv6 的那些事儿

原文连接

为了加深印象与理解,自己copy了一份,顺便按照自己的理解稍作修改。
参考资料

自从5月初Apple明文规定所有开发者在6月1号以后提交的新版本都需要支持IPv6-Only的网络。公司的App在5月30号提交审核,悲催的被拒了,原因是个别第三方库没有支持IPv6。

一、IPv6-Only支持是啥?

首先IPv6,是对IPv4地址空间的扩充,目前IP协议的版本号是4(简称为IPv4),它的下一个版本就是IPv6。

从理论上讲,IPv4编址1600万个网络、40亿台主机。IPv6所拥有的地址容量是IPv4的约8×1028倍,达到2128(算上全零的)个。

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

NAT64-DNS64-ResolutionOfIPv4.png

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

local_ipv6_dns64_nat64_network.png

二、Apple如何审核支持IPV6-Only?

(1)、这里说的支持IPv6-Only网络,其实就是说让应用在 IPv6 DNS64/NAT64 网络环境下仍然能够正常运行。但是考虑到我们目前的实际网络环境仍然是IPv4网络,所以应用需要能够同时保证IPv4和IPv6环境下的可用性。从这点来说,苹果不会去扫描IPv4的专有API来拒绝审核通过,因为IPv4的API和IPv6的API调用都会同时存在于代码中(不过为了减小审核被拒风险,建议将IPv4专有API通过IPv6的兼容API来替换)。
(2)、Apple官方声明iOS9开始向IPv6支持过渡,在iOS9.2+支持通过getaddrInfo方法将IPv4地址合成IPv6地址(The ability to synthesize IPv6 addresses was added to getaddrinfo in iOS 9.2 and OS X 10.11.2)。其提供的Reachability库在iOS8系统下,当从IPv4切换到IPv6网络,或者从IPv6网络切换到IPv4,是无法监控到网络状态的变化。也有一些开发者针对这些Bug询问Apple的审核部门,给予的答复是只需要在苹果最新的系统上保证IPv6的兼容性即可
(3)、只要应用的主流程支持IPv6,通过苹果审核即可。对于不支持IPv6的模块,考虑到我们现实IPv6网络的部署还需要一段时间,短时间内不会影响我们用户的使用。但随着4G网络IPv6的部署,这部分模块还是需要逐渐安排人力进行支持。
(4)、如果应用一直直接使用IPv4地址通过NSURLConenction或者NSURLSession进行网络请求(一般需要服务器允许,且客户端需要在header中伪装host);经测试,IPv6网络环境下,直接使用IPV4地址在iOS9及以上的系统仍然能够正常访问;在iOS8.4及以下不能正常访问;这一点苹果的解释和建议是这样的:

Note: In iOS 9 and OS X 10.11 and later, NSURLSession and CFNetwork automatically synthesize IPv6 addresses from IPv4 literals locally on devices operating on DNS64/NAT64 networks. However, you should still work to rid your code of IP address literals.

三、应用如何支持IPv6-Only?

对于如何支持IPv6-Only,官方给出了如下几点标准:

1. Use High-Level Networking Frameworks;
2. Don’t Use IP Address Literals;
3. Check Source Code for IPv6 DNS64/NAT64 Incompatibilities;
4. Use System APIs to Synthesize IPv6 Addresses;

3.1 NSURLConnection是否支持IPv6?

官方的这句话让我们疑惑顿生:
*** using high-level networking APIs such as NSURLSession and the CFNetwork frameworks and you connect by name, you should not need to change anything for your app to work with IPv6 addresses***
只说了NSURLSession和CFNetwork的API不需要改变,但是并没有提及到NSURLConnection。 从上文的参考资料中,我们看到NSURLSession、NSURLConnection同属于Cocoa的url loading system,可以猜测出NSURLConnection在iOS9上是支持IPv6的。
应用里面的API网络请求,大家一般都会选择AFNetworking进行请求发送,由于历史原因,应用的代码基本上都深度引用了AFHTTPRequestOperation类,所以目前API网络请求均需要通过NSURLConnection发送出去,所以必须确认NSURLConnection是否支持IPv6。经过测试,NSURLConnection在最新的iOS9系统上是支持IPv6的,但AFNetworking还需要测试。(最新版本应该没有问题,6月1日提交审核App成功通过)

3.2 Reachability是否需要修改支持IPv6?

我们可以查到应用中大量使用了Reachability进行网络状态判断,但是在里面却使用了IPv4的专用API。
AFNetworking的AFNetworkReachabilityManager对IPv6进行的处理

AFNetworkReachabilityManager.m[145]
+ (instancetype)manager
{
#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 90000) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
    struct sockaddr_in6 address;
    bzero(&address, sizeof(address));
    address.sin6_len = sizeof(address);
    address.sin6_family = AF_INET6;
#else
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_len = sizeof(address);
    address.sin_family = AF_INET;
#endif
    return [self managerForAddress:&address];
}

苹果在iOS9以上对Zero Address进行了特别处理,官方发言是这样的:

reachabilityForInternetConnection: This monitors the address 0.0.0.0,
which reachability treats as a special token that causes it to actually
monitor the general routing status of the device, both IPv4 and IPv6.

经过我们测试,AFNetworking已经支持IPv6,目前没测试出存在什么问题,AppStore也通过审核。

四、底层的socket API如何同时支持IPv4和IPv6?

由于在应用中使用了网络诊断的组件,大量使用了底层的 socket API,所以对于IPV6支持,这块是个重头戏。如果你的应用中使用了长连接,其必然会使用底层socket 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:(struct in6_addr)ipv6Addr{
    NSString *address = nil; 
    char dstStr[INET6_ADDRSTRLEN]; 
    char srcStr[INET6_ADDRSTRLEN]; 
    memcpy(srcStr, &ipv6Addr, sizeof(struct in6_addr));
    if(inet_ntop(AF_INET6, srcStr, dstStr, INET6_ADDRSTRLEN) != NULL){ 
        address = [NSString stringWithUTF8String:dstStr]; 
    } 
    return address;
}

//for IPV4
+(NSString *)formatIPV4Address:(struct in_addr)ipv4Addr{
    NSString *address = nil; 
    char dstStr[INET_ADDRSTRLEN]; 
    char srcStr[INET_ADDRSTRLEN]; 
    memcpy(srcStr, &ipv4Addr, sizeof(struct in_addr));
    if(inet_ntop(AF_INET, srcStr, dstStr, INET_ADDRSTRLEN) != NULL){ 
        address = [NSString stringWithUTF8String:dstStr]; 
    } 
    return address;
}

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 iPhone 
        if ([[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 String 
                address = [self formatIPV4Address:((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr]; 
            } 
            //如果是IPv6地址 
            else if (temp_addr->ifa_addr->sa_family == AF_INET6){ 
                address = [self formatIPV6Address:((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 */ 
{
    int mib[] = {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 IPV4 
    for (i = 0; i < RTAX_MAX; i++) { 
        if (rt->rtm_addrs & (1 << i)) { 
            sa_tab[i] = sa; 
            sa = (struct sockaddr *)((char *)sa + ROUNDUP(sa->sa_len)); 
        } else { 
            sa_tab[i] = NULL; 
        } 
    } 

    //for IPV6 
    for (i = 0; i < RTAX_MAX; i++) { 
        if (rt->rtm_addrs & (1 << i)) { 
            sa_tab[i] = sa; 
            sa = (struct sockaddr_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)); 
    int result = res_ninit(res); 
    NSMutableArray *servers = [[NSMutableArray alloc] init]; 
    if (result == 0) { 
        union res_9_sockaddr_union *addr_union = malloc(res->nscount * sizeof(union res_9_sockaddr_union));
        res_getservers(res, addr_union, res->nscount); 
        for (int i = 0; i < res->nscount; i++) { 
            if (addr_union[i].sin.sin_family == AF_INET) { 
                char ip[INET_ADDRSTRLEN]; 
                inet_ntop(AF_INET, &(addr_union[i].sin.sin_addr), ip, INET_ADDRSTRLEN); 
                NSString *dnsIP = [NSString stringWithUTF8String:ip]; 
                [servers addObject:dnsIP]; 
                NSLog(@"IPv4 DNS IP: %@", dnsIP); 
            } else if (addr_union[i].sin6.sin6_family == AF_INET6) { 
                char ip[INET6_ADDRSTRLEN]; 
                inet_ntop(AF_INET6, &(addr_union[i].sin6.sin6_addr), ip, INET6_ADDRSTRLEN);   
                NSString *dnsIP = [NSString stringWithUTF8String:ip]; 
                [servers addObject:dnsIP]; 
                NSLog(@"IPv6 DNS IP: %@", dnsIP); 
            } else { 
                NSLog(@"Undefined family."); 
            } 
        } 
    } 
    res_nclose(res); free(res); 
    return [NSArray arrayWithArray:servers];
}

4.5 域名DNS地址获取支持IPV6

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

//ipv4
phot = gethostbyname(hostN);

//ipv6 
phot = gethostbyname2(hostN, AF_INET6);

4.6 ping方案支持IPv6

Apple的官方提供了最新的支持IPv6的ping方案,参考地址如下:https://developer.apple.com/library/mac/samplecode/SimplePing/Introduction/Intro.html
只是需要注意的是:
(1)返回的packet去掉了IPHeader部分,IPv6的header部分也不返回TTL(Time to Live)字段;
(2)IPv6的ICMP报文不进行checkSum的处理;

4.7 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 Invalid argument的错误。
关键代码如下:(完整代码参考开源组件)

//构造通用的IP地址结构stuck sockaddr 
{
    NSString *ipAddr0 = [serverDNSs objectAtIndex:0]; 
    //设置server主机的套接口地址 
    NSData *addrData = nil; 
    BOOL isIPV6 = NO; 
    if ([ipAddr0 rangeOfString:@":"].location == NSNotFound) { 
        isIPV6 = NO; 
        struct sockaddr_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 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; 
    } else { 
        isIPV6 = YES; 
        struct sockaddr_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 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; 
    } 
    struct sockaddr *destination; 
    destination = (struct sockaddr *)[addrData bytes];

    //创建socket
    if ((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 套接字的ttl
    if ((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, (struct sockaddr *)destination, isIPV6?sizeof(struct sockaddr_in6):sizeof(struct sockaddr_in));

五、本地代码具体支持IPv6的一些办法

5.1不建议使用底层的网络API

下图展示的蓝色部分的这些API都是不存在兼容性问题的,而我们平时自己用的包括那些第三方的网络库大部分都是用的这些API。

Networking frameworks and API layers.png

大部分情况下,我们用高级的API完全能够实现我们的需求,而且高级API封装的很便于使用,很多底层的像适配IPv6的工作都已经帮我们做好了。而用底层API会有大量的工作要我们自己来做,更容易产生bug。但你如果确实需要用底层的POSIX socket API, 请参照这个RFC4038: Application Aspects of IPv6 Transition的指导。

5.2不要用IP地址

比如下面这个API,nodename这个参数不要传IP地址,而应该用域名

SCNetworkReachabilityCreateWithName.png

这个方法在著名的Reachability中是用到的,我们常用的网络库AFNetworking就用了这个。所以用到的同学得好好查一下了,另外这个项目的作者几天前刚刚就这个问题有一个新的提交,不过最新的release版本中还没有加进去,可以点下面链接先去看看他都改了哪些地方。
Added support for IPv6 to Reachability #3174

5.3检查不兼容IPv6的代码

搜一下工程里有没有下面的这些API,这些都是只针对IPv4做处理的,有的话就删了。

inet_addr()
inet_aton()
inet_lnaof()
inet_makeaddr()
inet_netof()
inet_network()
inet_ntoa()
inet_ntoa_r()
bindresvport()
getipv4sourcefilter()
setipv4sourcefilter()

如果用到了下面左边的这些IPv4的类型,那么它们相应的IPv6类型也需要做处理

IPv4-IPv6.png

5.4

搭建一个IPv6的测试环境,你所需要的就是一台用非Wi-Fi的方式上网的Mac电脑。
我们的要做的其实就是用Mac做一个热点,然后用iPhone连接这个Wi-Fi,听起来很容易,我相信大家在公司就是这么干的吧。
区别是这次我们产生的是一个本地的IPv6 DNS64/NAT64网络,这项功能是OS X 10.11新加的。和我们以前开启热点方式不一样的地方在于,我们在“System Preferences”界面选中“Sharing”的同时,要按住“Option”键。

**System Preferences**.png

之后在“Sharing”界面中,我们会看到和之前不一样的地方,就是红框所标的地方,多了一个叫“Create NAT64 Network”的选框,选中它。

**Sharing**.png

之后就是按照正常的创建热点的流程走完就行了。
现在我们用iPhone连接上这个刚创建好的热点就可以测试了。

注意:
一定要保证只用上面创建的Wi-Fi上网。

推荐阅读更多精彩内容