iOS版應用支援IPV6,就那點事兒

Geek.發表於2019-04-27

果然是蘋果打個哈欠,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…

相關文章