原文連接
為了加深印象與理解,自己copy了一份,順便按照自己的理解稍作修改。
參考資料
自從5月初Apple明文規(guī)定所有開(kāi)發(fā)者在6月1號(hào)以后提交的新版本都需要支持IPv6-Only的網(wǎng)絡(luò)。公司的App在5月30號(hào)提交審核,悲催的被拒了,原因是個(gè)別第三方庫(kù)沒(méi)有支持IPv6。
一、IPv6-Only支持是啥?
首先IPv6,是對(duì)IPv4地址空間的擴(kuò)充,目前IP協(xié)議的版本號(hào)是4(簡(jiǎn)稱(chēng)為IPv4),它的下一個(gè)版本就是IPv6。
從理論上講,IPv4編址1600萬(wàn)個(gè)網(wǎng)絡(luò)、40億臺(tái)主機(jī)。IPv6所擁有的地址容量是IPv4的約8×1028倍,達(dá)到2128(算上全零的)個(gè)。
目前當(dāng)我們用iOS設(shè)備連接上Wifi、4G、3G等網(wǎng)絡(luò)時(shí),設(shè)備被分配的地址均是IPv4地址,但是隨著運(yùn)營(yíng)商和企業(yè)逐漸部署IPv6 DNS64/NAT64網(wǎng)絡(luò)之后,設(shè)備被分配的地址會(huì)變成IPV6的地址,而這些網(wǎng)絡(luò)就是所謂的IPv6-Only網(wǎng)絡(luò),并且仍然可以通過(guò)此網(wǎng)絡(luò)去獲取IPV4地址提供的內(nèi)容。客戶(hù)端向服務(wù)器端請(qǐng)求域名解析,首先通過(guò)DNS64 Server查詢(xún)IPv6的地址,如果查詢(xún)不到,再向DNS Server查詢(xún)IPv4地址,通過(guò)DNS64 Server合成一個(gè)IPv6的地址,最終將一個(gè)IPV6的地址返回給客戶(hù)端。如圖所示:
在Mac OS 10.11+的雙網(wǎng)卡的Mac機(jī)器(以太網(wǎng)口+無(wú)線網(wǎng)卡),我們可以通過(guò)模擬構(gòu)建這么一個(gè)local IPv6 DNS64/NAT64 的網(wǎng)絡(luò)環(huán)境去測(cè)試應(yīng)用是否支持IPV6-Only網(wǎng)絡(luò),大概原理如下:
二、Apple如何審核支持IPV6-Only?
(1)、這里說(shuō)的支持IPv6-Only網(wǎng)絡(luò),其實(shí)就是說(shuō)讓?xiě)?yīng)用在 IPv6 DNS64/NAT64 網(wǎng)絡(luò)環(huán)境下仍然能夠正常運(yùn)行。但是考慮到我們目前的實(shí)際網(wǎng)絡(luò)環(huán)境仍然是IPv4網(wǎng)絡(luò),所以應(yīng)用需要能夠同時(shí)保證IPv4和IPv6環(huán)境下的可用性。從這點(diǎn)來(lái)說(shuō),蘋(píng)果不會(huì)去掃描IPv4的專(zhuān)有API來(lái)拒絕審核通過(guò),因?yàn)镮Pv4的API和IPv6的API調(diào)用都會(huì)同時(shí)存在于代碼中(不過(guò)為了減小審核被拒風(fēng)險(xiǎn),建議將IPv4專(zhuān)有API通過(guò)IPv6的兼容API來(lái)替換)。
(2)、Apple官方聲明iOS9開(kāi)始向IPv6支持過(guò)渡,在iOS9.2+支持通過(guò)getaddrInfo方法將IPv4地址合成IPv6地址(The ability to synthesize IPv6 addresses was added to getaddrinfo in iOS 9.2 and OS X 10.11.2)。其提供的Reachability庫(kù)在iOS8系統(tǒng)下,當(dāng)從IPv4切換到IPv6網(wǎng)絡(luò),或者從IPv6網(wǎng)絡(luò)切換到IPv4,是無(wú)法監(jiān)控到網(wǎng)絡(luò)狀態(tài)的變化。也有一些開(kāi)發(fā)者針對(duì)這些Bug詢(xún)問(wèn)Apple的審核部門(mén),給予的答復(fù)是只需要在蘋(píng)果最新的系統(tǒng)上保證IPv6的兼容性即可。
(3)、只要應(yīng)用的主流程支持IPv6,通過(guò)蘋(píng)果審核即可。對(duì)于不支持IPv6的模塊,考慮到我們現(xiàn)實(shí)IPv6網(wǎng)絡(luò)的部署還需要一段時(shí)間,短時(shí)間內(nèi)不會(huì)影響我們用戶(hù)的使用。但隨著4G網(wǎng)絡(luò)IPv6的部署,這部分模塊還是需要逐漸安排人力進(jìn)行支持。
(4)、如果應(yīng)用一直直接使用IPv4地址通過(guò)NSURLConenction或者NSURLSession進(jìn)行網(wǎng)絡(luò)請(qǐng)求(一般需要服務(wù)器允許,且客戶(hù)端需要在header中偽裝host);經(jīng)測(cè)試,IPv6網(wǎng)絡(luò)環(huán)境下,直接使用IPV4地址在iOS9及以上的系統(tǒng)仍然能夠正常訪問(wèn);在iOS8.4及以下不能正常訪問(wèn);這一點(diǎn)蘋(píng)果的解釋和建議是這樣的:
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.
三、應(yīng)用如何支持IPv6-Only?
對(duì)于如何支持IPv6-Only,官方給出了如下幾點(diǎn)標(biāo)準(zhǔn):
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***
只說(shuō)了NSURLSession和CFNetwork的API不需要改變,但是并沒(méi)有提及到NSURLConnection。 從上文的參考資料中,我們看到NSURLSession、NSURLConnection同屬于Cocoa的url loading system,可以猜測(cè)出NSURLConnection在iOS9上是支持IPv6的。
應(yīng)用里面的API網(wǎng)絡(luò)請(qǐng)求,大家一般都會(huì)選擇AFNetworking進(jìn)行請(qǐng)求發(fā)送,由于歷史原因,應(yīng)用的代碼基本上都深度引用了AFHTTPRequestOperation類(lèi),所以目前API網(wǎng)絡(luò)請(qǐng)求均需要通過(guò)NSURLConnection發(fā)送出去,所以必須確認(rèn)NSURLConnection是否支持IPv6。經(jīng)過(guò)測(cè)試,NSURLConnection在最新的iOS9系統(tǒng)上是支持IPv6的,但AFNetworking還需要測(cè)試。(最新版本應(yīng)該沒(méi)有問(wèn)題,6月1日提交審核App成功通過(guò))
3.2 Reachability是否需要修改支持IPv6?
我們可以查到應(yīng)用中大量使用了Reachability進(jìn)行網(wǎng)絡(luò)狀態(tài)判斷,但是在里面卻使用了IPv4的專(zhuān)用API。
AFNetworking的AFNetworkReachabilityManager對(duì)IPv6進(jìn)行的處理
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];
}
蘋(píng)果在iOS9以上對(duì)Zero Address進(jìn)行了特別處理,官方發(fā)言是這樣的:
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.
經(jīng)過(guò)我們測(cè)試,AFNetworking已經(jīng)支持IPv6,目前沒(méi)測(cè)試出存在什么問(wèn)題,AppStore也通過(guò)審核。
四、底層的socket API如何同時(shí)支持IPv4和IPv6?
由于在應(yīng)用中使用了網(wǎng)絡(luò)診斷的組件,大量使用了底層的 socket API,所以對(duì)于IPV6支持,這塊是個(gè)重頭戲。如果你的應(yīng)用中使用了長(zhǎng)連接,其必然會(huì)使用底層socket API,這一塊也是需要支持IPv6的。 對(duì)于Socket如何同時(shí)支持IPv4和IPv6,可以參考谷歌的開(kāi)源庫(kù)CocoaAsyncSocket.
下面我針對(duì)我們的開(kāi)源 網(wǎng)絡(luò)診斷組件, 說(shuō)一下是如何同時(shí)支持IPV4和IPV6的。
開(kāi)源地址:https://github.com/Lede-Inc/LDNetDiagnoService_IOS.git這個(gè)網(wǎng)絡(luò)診斷組件的主要功能如下:
本地網(wǎng)絡(luò)環(huán)境的監(jiān)測(cè)(本機(jī)IP+本地網(wǎng)關(guān)+本地DNS+域名解析);
通過(guò)TCP Connect監(jiān)測(cè)到域名的連通性;
通過(guò)Ping 監(jiān)測(cè)到目標(biāo)主機(jī)的連通耗時(shí);
通過(guò)traceRoute監(jiān)測(cè)設(shè)備到目標(biāo)主機(jī)中間每一個(gè)路由器節(jié)點(diǎn)的ICMP耗時(shí);
4.1 IP地址從二進(jìn)制到符號(hào)的轉(zhuǎn)化
之前我們都是通過(guò)inet_ntoa()進(jìn)行二進(jìn)制到符號(hào),這個(gè)API只能轉(zhuǎn)化IPV4地址。而inet_ntop()能夠兼容轉(zhuǎn)化IPv4和IPv6地址。 寫(xiě)了一個(gè)公用的in6_addr的轉(zhuǎn)化方法如下:
//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 本機(jī)IP獲取支持IPv6
相當(dāng)于我們?cè)诮K端中輸入ifconfig命令獲取字符串,然后對(duì)ifconfig結(jié)果字符串進(jìn)行解析,獲取其中en0(Wifi)、pdp_ip0(移動(dòng)網(wǎng)絡(luò))的ip地址。
注意:
(1)在模擬器和真機(jī)上都會(huì)出現(xiàn)以FE80開(kāi)頭的IPv6單播地址影響我們判斷,所以在這里進(jìn)行特殊的處理(當(dāng)?shù)谝淮斡龅讲皇菃尾サ刂返腎P地址即為本機(jī)IP地址)。
(2)在IPv6環(huán)境下,真機(jī)測(cè)試的時(shí)候,第一個(gè)出現(xiàn)的是一個(gè)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地址,直接轉(zhuǎn)化
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 設(shè)備網(wǎng)關(guān)地址獲取獲取支持IPv6
其實(shí)是在IPV4獲取網(wǎng)關(guān)地址的源碼的基礎(chǔ)上進(jìn)行了修改,初開(kāi)把AF_INET->AF_INET6, sockaddr -> sockaddr_in6之外,還需要注意如下修改,就是拷貝的地址字節(jié)數(shù)。去掉了ROUNDUP的處理。 (解析出來(lái)的地址老是少了4個(gè)字節(jié),結(jié)果是偏移量搞錯(cuò)了,糾結(jié)了半天),具體參考源碼庫(kù)。
/* 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 設(shè)備DNS地址獲取支持IPv6
IPv4時(shí)只需要通過(guò)res_ninit進(jìn)行初始化就可以獲取,但是在IPV6環(huán)境下需要通過(guò)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網(wǎng)絡(luò)下我們通過(guò)gethostname獲取,而在IPv6環(huán)境下,通過(guò)新的gethostbyname2函數(shù)獲取。
//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報(bào)文不進(jìn)行checkSum的處理;
4.7 traceRoute方案支持IPv6
其實(shí)是通過(guò)創(chuàng)建socket套接字模擬ICMP報(bào)文的發(fā)送,以計(jì)算耗時(shí);兩個(gè)關(guān)鍵的地方需要注意:
(1)IPv6中去掉IP_TTL字段,改用跳數(shù)IPv6_UNICAST_HOPS來(lái)表示;
(2)sendto方法可以兼容支持IPv4和IPv6,但是需要最后一個(gè)參數(shù),制定目標(biāo)IP地址的大小;因?yàn)榍耙粋€(gè)參數(shù)只是指明了IP地址的開(kāi)始地址。千萬(wàn)不要用統(tǒng)一的sizeof(struct sockaddr), 因?yàn)閟ockaddr_in 和 sockaddr都是16個(gè)字節(jié),兩者可以通用,但是sockaddr_in6的數(shù)據(jù)結(jié)構(gòu)是28個(gè)字節(jié),如果不顯式指定,sendto方法就會(huì)一直返回-1,erroNo報(bào)22 Invalid argument的錯(cuò)誤。
關(guān)鍵代碼如下:(完整代碼參考開(kāi)源組件)
//構(gòu)造通用的IP地址結(jié)構(gòu)stuck sockaddr
{
NSString *ipAddr0 = [serverDNSs objectAtIndex:0];
//設(shè)置server主機(jī)的套接口地址
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];
//創(chuàng)建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)
//設(shè)置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)
//發(fā)送成功返回值等于發(fā)送消息的長(zhǎng)度
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不建議使用底層的網(wǎng)絡(luò)API
下圖展示的藍(lán)色部分的這些API都是不存在兼容性問(wèn)題的,而我們平時(shí)自己用的包括那些第三方的網(wǎng)絡(luò)庫(kù)大部分都是用的這些API。
大部分情況下,我們用高級(jí)的API完全能夠?qū)崿F(xiàn)我們的需求,而且高級(jí)API封裝的很便于使用,很多底層的像適配IPv6的工作都已經(jīng)幫我們做好了。而用底層API會(huì)有大量的工作要我們自己來(lái)做,更容易產(chǎn)生bug。但你如果確實(shí)需要用底層的POSIX socket API, 請(qǐng)參照這個(gè)RFC4038: Application Aspects of IPv6 Transition的指導(dǎo)。
5.2不要用IP地址
比如下面這個(gè)API,nodename這個(gè)參數(shù)不要傳IP地址,而應(yīng)該用域名
這個(gè)方法在著名的Reachability中是用到的,我們常用的網(wǎng)絡(luò)庫(kù)AFNetworking就用了這個(gè)。所以用到的同學(xué)得好好查一下了,另外這個(gè)項(xiàng)目的作者幾天前剛剛就這個(gè)問(wèn)題有一個(gè)新的提交,不過(guò)最新的release版本中還沒(méi)有加進(jìn)去,可以點(diǎn)下面鏈接先去看看他都改了哪些地方。
Added support for IPv6 to Reachability #3174
5.3檢查不兼容IPv6的代碼
搜一下工程里有沒(méi)有下面的這些API,這些都是只針對(duì)IPv4做處理的,有的話就刪了。
inet_addr()
inet_aton()
inet_lnaof()
inet_makeaddr()
inet_netof()
inet_network()
inet_ntoa()
inet_ntoa_r()
bindresvport()
getipv4sourcefilter()
setipv4sourcefilter()
如果用到了下面左邊的這些IPv4的類(lèi)型,那么它們相應(yīng)的IPv6類(lèi)型也需要做處理
5.4
搭建一個(gè)IPv6的測(cè)試環(huán)境,你所需要的就是一臺(tái)用非Wi-Fi的方式上網(wǎng)的Mac電腦。
我們的要做的其實(shí)就是用Mac做一個(gè)熱點(diǎn),然后用iPhone連接這個(gè)Wi-Fi,聽(tīng)起來(lái)很容易,我相信大家在公司就是這么干的吧。
區(qū)別是這次我們產(chǎn)生的是一個(gè)本地的IPv6 DNS64/NAT64網(wǎng)絡(luò),這項(xiàng)功能是OS X 10.11新加的。和我們以前開(kāi)啟熱點(diǎn)方式不一樣的地方在于,我們?cè)凇?strong>System Preferences”界面選中“Sharing”的同時(shí),要按住“Option”鍵。
之后在“Sharing”界面中,我們會(huì)看到和之前不一樣的地方,就是紅框所標(biāo)的地方,多了一個(gè)叫“Create NAT64 Network”的選框,選中它。
之后就是按照正常的創(chuàng)建熱點(diǎn)的流程走完就行了。
現(xiàn)在我們用iPhone連接上這個(gè)剛創(chuàng)建好的熱點(diǎn)就可以測(cè)試了。
注意:
一定要保證只用上面創(chuàng)建的Wi-Fi上網(wǎng)。