天天看點

iOS 應用支援 IPv6

iOS 行業内就得起一次風暴呀。自從 5 月初 Apple 明文規定所有開發者在 6 月 1 号以後送出新版本需要支援 IPv6-Only 的網絡,大家便開始熱火朝天的研究如何支援 IPv6,以及應用中哪些子產品目前不支援 IPv6。

一、IPv6-Only 支援是啥?

首先 IPv6,是對 IPv4 位址空間的擴充。目前當我們用 iOS 裝置連接配接上 Wifi、4G、3G 等網絡時,裝置被配置設定的位址均是 IPv4 位址,但是随着營運商和企業逐漸部署 IPv6 DNS64/NAT64 網絡之後,裝置被配置設定的位址會變成 IPv6 的位址,而這些網絡就是所謂的 IPv6-Only 網絡,并且仍然可以通過此網絡去擷取 IPv4 位址提供的内容。用戶端向伺服器端請求域名解析,首先通過 DNS64 Server 查詢 IPv6 的位址,如果查詢不到,再向 DNS Server 查詢 IPv4 位址,通過 DNS64 Server 合成一個 IPv6 的位址,最終将一個 IPv6 的位址傳回給用戶端。

在 Mac OS 10.11+的雙網卡的 Mac 機器(以太網口+無線網卡),我們可以通過模拟建構這麼一個 local IPv6 DNS64/NAT64 的網絡環境去測試應用是否支援 IPv6-Only 網絡,

  • 參考資料:
  • https://developer.apple.com/library/mac/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html#//apple_ref/doc/uid/TP40010220-CH213-SW1

二、Apple 如何稽核支援 IPv6-Only?

首先第一點: 這裡說的支援 IPv6-Only 網絡,其實就是說讓應用在 IPv6 DNS64/NAT64 網絡環境下仍然能夠正常運作。但是考慮到我們目前的實際網絡環境仍然是 IPv4 網絡,是以應用需要能夠同時保證 IPv4 和 IPv6 環境下的可用性。從這點來說,蘋果不會去掃描 IPv4 的專有 API 來拒絕稽核通過,因為 IPv4 的 API 和 IPv6 的 API 調用都會同時存在于代碼中。

其次第二點:Apple 官方聲明 iOS9 開始向 IPv6 支援過渡,在 iOS9.2+ 支援 IPv4 位址合成 IPv6 位址。其提供的 Reachability 庫在 iOS8 系統下,當從 IPv4 切換到 IPv6 網絡,或者從 IPv6 網絡切換到 IPv4,是無法監控到網絡狀态的變化。也有一些開發者針對這些 Bug 詢問 Apple 的稽核部門, 給予的答複是隻需要在蘋果最新的系統上保證 IPv6 的相容性即可 。

最後第三點: 隻要應用的主流程支援 IPv6,通過蘋果稽核即可。對于不支援 IPv6 的子產品,考慮到我們現實 IPv6 網絡的部署還需要一段時間,短時間内不會影響我們使用者的使用。但随着 4G 網絡 IPv6 的部署,這部分子產品還是需要逐漸安排人力進行支援。

三、應用如何支援 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 的。

3.2 Cocoa 的 URL Loading System 從 iOS 哪個版本開始支援 IPv6?

目前我們的應用最低版本還需要支援 iOS7,雖然蘋果隻要求最新版本支援 IPv6-Only,但是出于對使用者負責的态度,我們仍然需要搞清楚在低版本上 URL Loading System 的 API 是否支援 IPv6.

(to fix me, make some experiments)待續~~~

3.3 Reachability 是否需要修改支援 IPv6?

我們可以查到應用中大量使用了 Reachability 進行網絡狀态判斷,但是在裡面卻使用了 IPv4 的專用 API。

在 Pods:Reachability 中
AF_INET                  Files:Reachability.m
struct sockaddr_in       Files:Reachability.h , Reachability.m
           

那 Reachability 應該如何支援 IPv6 呢?

(1)目前 Github 的開源庫 Reachability 的最新版本是 3.2,蘋果也出了一個 Support IPv6 的 Reachability 的官方樣例,我們比較了一下源碼,跟 Github 上的 Reachability 沒有什麼差異。

(2)我們通常都是通過一個 0.0.0.0 (ZeroAddress) 去開啟網絡狀态監控,經過我們測試,在 iOS9 以上的系統上 IPv4 和 IPv6 網絡環境均能夠正常使用;但是在 iOS8 上 IPv4 和 IPv6 互相切換的時候無法監控到網絡狀态的變化,可能是因為蘋果在 iOS8 上還并沒有對 IPv6 進行相關支援相關。(但是這仍然滿足蘋果要求在最新系統版本上支援 IPv6 的網絡)。

(3)當大家都在要求 Reachability 添加對于 IPv6 的支援,其實蘋果在 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.

+ (instancetype)reachabilityForInternetConnection {
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
return [self reachabilityWithAddress: (const struct sockaddr *) &zeroAddress];
}
           

綜上所述,Reachability 不需要做任何修改,在 iOS9 上就可以支援 IPv6 和 IPv4,但是在 iOS9 以下會存在 bug,但是蘋果稽核并不關心。

四、底層的 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 的轉化方法如下:

+(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;
}
           

4.2 本機 IP 擷取支援 IPv6

相當于我們在終端中輸入 ifconfig 指令擷取字元串,然後對 ifconfig 結果字元串進行解析,擷取其中 en0(模拟器)、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 = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
//                    if (address && ![address isEqualToString:@""] && ![address.uppercaseString hasPrefix:@"FE80"]) break;
}

// 如果是 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.4 域名 DNS 位址擷取支援 IPv6

在 IPv4 網絡下我們通過 gethostname 擷取,而在 IPv6 環境下,通過新的 gethostbyname2 函數擷取。

//ipv4
phot = gethostbyname(hostN);

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

4.5 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.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 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);
nativeAddr4.sin_addr.s_addr = inet_addr([ipAddr0 UTF8String]);
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));