摘要:假定用戶只有 IPv6 地址,DNS 也是使用 IPv6 地址 (DNS 必須有雙棧環境,因爲現在很多權威服務器沒有 IPv6 地址,純 IPv6 環境下無法正常工作),假定一個服務通過域名(同時擁有 A、AAAA 記錄)對外提供服務,測試服務是否可以正常訪問。// 獲取系統允許的 IPv4 或者 IPv6 地址       /* Perform the request, res will get the return code */       res = curl_easy_perform(curl)。

董濤,網易遊戲高級運維工程師,主要工作方向爲網易集團 DNS 的運維與開發。

張欣接,網易集團 DNS 團隊負責人,負責網易域名系統的架構設計及生態建設。

一、IPv6 支持度報告

IPv6 簡介

IPv6(Internet Protocol version 6,互聯網通信協議第 6 版)是用於數據包交換互聯網絡的網絡層協議,是 IETF(互聯網工程任務小組 Internet Engineering Task Force,簡稱 IETF)設計的用來替代 IPv4 協議的互聯網協議版本。

隨着電子技術及網絡技術的發展,計算機網絡已經與人們的生活密切相關,可能身邊的每一樣電子設備都需要連入網絡,IPv4 的地址數量已經無法滿足。IPv6 的應用將徹底解決這些問題。IPv6 由 128 比特位構成,單從數量級上來說,IPv6 所擁有的地址容量是 IPv4 的約 8×10 28 倍,達到 2  128 (約 3.4 × 10  38 )個。這不但解決了網絡地址資源數量的問題,同時也爲物聯網的發展提供了基礎。

IPv6 地址的表達形式採用 32 個十六進制數,由兩個邏輯部分組成:一個 64 位的網絡前綴和一個 64 位的主機地址,主機地址通常根據物理地址自動生成,叫做 EUI-64(或者 64- 位擴展唯一標識)。例如 2001:0db8:85a3:08d3:1319:8a2e:0370:7344 是一個合法的 IPv6 地址。

IPv6 全球部署更新

  • 2008 年,歐盟發佈了“歐洲部署 IPv6 行動計劃”

  • 2009 年,日本發佈《IPv6 行動計劃》

  • 2010 年,美國政府發佈 IPv6 行動計劃

  • 2010 年,韓國發布“下一代互聯網協議(IPv6) 促進計劃”

  • 2012 年,加拿大政府發佈了《加拿大政府 IPv6 戰略》

  • 2017 年,國務院辦公廳印發《推進互聯網協議第六版(IPv6)規模部署行動計劃》

操作系統 IPv6 支持度

應用軟件 IPv6 支持度

客戶端軟件

1、瀏覽器

服務器軟件

1、程序開發軟件

2、數據庫

總結

毋庸置疑,下一代互聯網 IPv6 是萬物互連,智能化時代基礎網絡的重要支撐協議,但是從一個只擁有 IPv4 協議的巨型網絡要全面、平穩地過渡到一個純 IPv6 網絡需要一段極爲漫長的時間。從報告統計的數據來看,各種基礎軟件和應用軟件都已基本支持 IPv6。現在在國內的環境下,IPv6 的基礎環境還需要完善,爲此工信部也發佈了

《推進互聯網協議第六版(IPv6)規模部署行動計劃》(http://www.miit.gov.cn/n1146290/n4388791/c6166476/content.html)

推動各單位加快支持 IPv6。

IPv6 支持度報告的數據來源是:下一代國家互聯網中心在 2017 年 11 月發佈的 IPv6 支持度報告

(https://www.ipv6ready.org.cn/public/download/ipv6.pdf), 感興趣的同學可以查看原文。

二、IPv6 環境下 DNS 相關測試

背景介紹

名詞簡介

  • A 記錄

A 記錄是一個域名指向 IPv4 地址的解析結果,即最常見的記錄類型, 例如 ipv6test.ntes53.netease.com. 1800 IN A 123.58.166.70
  • AAAA 記錄

AAAA 是一個域名指向 IPv6 地址的解析結果。如果想要一個域名解析到 IPv6 地址,則需要設置此種類型的解析結果。同一個域名可以同時有 A 與 AAAA 兩種記錄類型, 例如 ipv6test.ntes53.netease.com. 1800 IN AAAA 2403:c80:100:3000::7b3a:a646
  • 緩存 DNS 服務器

用戶直接使用的 DNS 服務器,各種平臺、操作系統上直接設置的 DNS 服務器,常見的有 8.8.8.8, 114.114.114.114
  • 權威 DNS 服務器

用於域名的管理。權威 DNS 服務器只對自己所擁有的域名進行域名解析,對於不是自己的域名則拒絕應答。例如網易的權威 DNS 服務器只會響應網易域名的請求,對於其他域名,則拒絕應答。
  • 雙棧網絡環境

雙棧網絡環境即客戶端或服務器同時擁有 IPv4、IPv6 兩種網絡環境,可以簡單的理解爲機器上既有 IPv4 地址又有 IPv6 地址

測試場景

下文中所有測試使用的程序均爲測試方法中的程序

1.目前純 IPv4 環境下,僅新增 AAAA(IPv6) 記錄之後,對已有程序的影響

假定已經存在了一個程序(C 程序、python 程序、瀏覽器等),通過域名訪問某個服務,現在在 IPv4 環境下一切工作正常。當給這個域名增加了 AAAA 記錄之後,測試對目前的程序的影響。

域名解析

HTTP 請求

客戶端

結論

  • 當在某域名原有的 A 記錄類型的基礎上新增 AAAA 記錄後,原有的程序工作正常

2.客戶端 IPv6/v4 雙棧環境下,測試程序的行爲

假定用戶的環境是雙棧環境,假定一個服務通過域名對外提供服務,測試這種情況下程序的行爲。

域名解析

HTTP 請求

客戶端

結論

  • 當域名同時存在 A 與 AAAA 記錄,並且網絡類型爲雙棧網絡時,絕大多數程序工作正常。僅有一種情況例外,即程序中使用了 gethostbyname 函數,同時 resolv.conf 中配置了 options inet6 時,此時程序會返回錯誤的解析結果

  • RFC 以及絕大多數實現方式,均回優先使用 IPv6 地址建立連接

  • 雙棧環境下,客戶端使用 IPv4 與 IPv6 緩存 DNS 服務器獲取的解析結果是一致的

3. 客戶端純 IPv6 環境下,測試能否正常工作

假定用戶只有 IPv6 地址,DNS 也是使用 IPv6 地址 (DNS 必須有雙棧環境,因爲現在很多權威服務器沒有 IPv6 地址,純 IPv6 環境下無法正常工作),假定一個服務通過域名(同時擁有 A、AAAA 記錄)對外提供服務,測試服務是否可以正常訪問。

域名解析

HTTP 請求

客戶端

結論

當某域名即存在 A 記錄 又存在 AAAA 記錄時:

  • 如果程序中使用了 gethostbyname 時,程序可能會拿到錯誤的解析結果,取決於 resolv.conf 的配置(當配置了 option inet6 時,會獲取到錯誤的解析結果)

  • Windows 在這種情況下,部分應用工作不正常。在指定使用 IPv6 socket 的情況下,程序工作正常。

  • 根據安卓官方的描述,Android 6.0 之後的版本已經支持 IPv6,但是根據對國內大多數廠商的安卓手機的調研,目前國內安卓手機很少可以原生支持 IPv6

4. DNS 解析測試

這裏測試了緩存服務器和權威服務器在各種網絡環境下,優先使用的解析鏈路。

結論

當權威服務器和緩存服務器均支持 ipv6 時,緩存服務器優先使用 ipv6 鏈路進行解析,其他情況均使用 ipv4 鏈路進行解析。

結論

  • 經過測試與查證, gethostbyname  不支持 IPv6,使用此函數可能會拿到錯誤的結果或者程序拋出異常。建議使用  getaddrinfo  函數取代此函數

  • 目前已經存在 A 記錄的域名,添加 AAAA 記錄後,不管客戶端與服務端的網絡環境如何:

    • 絕大多數情況下對客戶端與服務端工作正常

    • 下面一種情況下會出現工作異常:

      當使用了 C 的 gethostbyname 並且在 resolv.conf 中配置了 options inet6時,此函數返回錯誤的結果

  • 經過測試,雙棧網絡下 IPv4 與 IPv6 的優先級:

    • 優先使用 IPv6 發起解析請求

    • 優先使用 IPv6 請求建立連接 (TCP, UDP)

    • 優先解析 A 地址記錄

參考資料

  • Windows 8 IPv4 與 IPv6 選擇的方法:Connecting with IPv6 in Windows8

    (https://blogs.msdn.microsoft.com/b8/2012/06/05/connecting-with-ipv6-in-windows-8/)

  • Windows 當 IPv6 不可用後的回退機制:Is there any setting for connection timeout when IPv6 fallback to IPv4?

    (https://social.technet.microsoft.com/Forums/en-US/d09e938a-a594-4766-8898-3926a81fc5dc/is-there-any-setting-for-connection-timeout-when-ipv6-fallback-to-ipv4?forum=w7itpronetworking)

  • 目前廣泛使用的 IPv4 與 IPv6 優先選擇算法爲 Happy Eyeballs

    (https://en.wikipedia.org/wiki/Happy_Eyeballs):

    • 目前使用此算法的項目有:Chrome, Opera 12.10, Firefox version 13, OS X, cURL

    • 此算法會優先選擇 IPv6 鏈路使用

    • 此算法的原理可參考 RFC 6555(Happy Eyeballs: Success with Dual-Stack Hosts)

      (https://tools.ietf.org/html/rfc6555)

    • 此算法的簡略工作流程如下:

    1. 當客戶端是雙棧環境時,客戶端會向緩存 DNS 服務器發起域名 A 記錄與 AAAA 記錄的解析請求,並受到解析結果,對應下圖中的 1-4

    2. 客戶端獲取到解析地址後,會同時使用 IPv4 與 IPv6 兩種鏈路嘗試建立連接,對應下圖中的 6-7。當 IPv6 鏈路比 IPv4 鏈路先建立連接,或者 IPv4 已經建立連接,但是在很短的時間間隔內,IPv6 也成功建立連接後,則這兩種情況下客戶端應該使用 IPv6 鏈路完成後續的網絡請求,對應圖中的 8-12

測試方法

解析域名

C/ C ++

  • gethostbyname

Linux

#include <stdio.h> 
    #include <netdb.h> 
    #include <arpa/inet.h> 
 
    int main(void) 
{ 
        int i = 0; 
        char str[32] = {0}; 
         struct hostent* phost = NULL; 
 
        phost = gethostbyname("IPv6test.ntes53.netease.com"); 
        printf("%s", inet_ntoa(*((struct in_addr*)phost->h_addr)));  
         
        return 0; 
    } 

Windows

 #include <winsock.h> 
    #include <Windows.h> 
    #include <stdio.h> 
 
    #pragma comment (lib, "ws2_32.lib") 
 
    int main(void) { 
        WSADATA wsaData = {0,}; 
        struct in_addr addr = {0,}; 
        struct hostent *res; 
        int i = 0; 
 
        WSAStartup(MAKEWORD(2, 2), &wsaData); 
 
        res = gethostbyname("IPv6test.ntes53.netease.com."); 
        while (res->h_addr_list[i] != 0) { 
            addr.s_addr = *(u_long *) res->h_addr_list[i++]; 
            printf("IP Address: %s\n", inet_ntoa(addr)); 
        } 
 
        WSACleanup(); 
} 

getaddrinfo

#include <stdio.h> 
    #include <string.h> 
    #include <stdlib.h> 
    #include <netdb.h> 
    #include <sys/types.h> 
    #include <sys/socket.h> 
    #include <arpa/inet.h> 
 
    int lookup_host () 
{ 
      struct addrinfo hints, *res; 
      int errcode; 
      char addrstr[100]; 
      void *ptr; 
 
      memset (&hints, 0, sizeof (hints)); 
      hints.ai_family = AF_INET; 
 
      errcode = getaddrinfo ("IPv6test.ntes53.netease.com", NULL, &hints, &res); 
      if (errcode != 0) 
        { 
          perror ("getaddrinfo"); 
          return -1; 
        } 
      while (res) 
        { 
          inet_ntop (res->ai_family, res->ai_addr->sa_data, addrstr, 100); 
          switch (res->ai_family) 
            { 
            case AF_INET: 
              ptr = &((struct sockaddr_in *) res->ai_addr)->sin_addr; 
              break; 
            case AF_INET6: 
              ptr = &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr; 
              break; 
            } 
          inet_ntop (res->ai_family, ptr, addrstr, 100); 
          printf ("IPv%d address: %s (%s)\n", res->ai_family == PF_INET6 ? 6 : 4, 
                  addrstr, res->ai_canonname); 
          res = res->ai_next; 
        } 
      return 0; 
    } 
    int main (void) 
{ 
        lookup_host(); 
    } 

windows

#define WIN32_LEAN_AND_MEAN 
    #define _WIN32_WINNT 0x501 
    #include <windows.h> 
    #include <winsock2.h> 
    #include <stdio.h> 
    #include <string.h> 
    #include <stdlib.h> 
    #include <sys/types.h> 
    #include <ws2tcpip.h> 
 
    #pragma comment (lib, "Ws2_32.lib") 
 
    // int iResult; 
    WSADATA wsaData; 
    int iResult = WSAStartup(MAKEWORD(2,2), &wsaData); 
 
    int inet_pton(int af, const char *src, void *dst) 
{ 
        struct sockaddr_storage ss; 
        int size = sizeof(ss); 
        char src_copy[INET6_ADDRSTRLEN+1]; 
 
        ZeroMemory(&ss, sizeof(ss)); 
        /* stupid non-const API */ 
        strncpy (src_copy, src, INET6_ADDRSTRLEN+1) 
        src_copy[INET6_ADDRSTRLEN] = 0; 
 
        if (WSAStringToAddress(src_copy, af, NULL, (struct sockaddr *)&ss, &size) == 0) { 
            switch(af) { 
                case AF_INET: 
                    *(struct in_addr *)dst = ((struct sockaddr_in *)&ss)->sin_addr; 
                    return 1; 
                case AF_INET6: 
                    *(struct in6_addr *)dst = ((struct sockaddr_in6 *)&ss)->sin6_addr; 
                    return 1; 
            } 
        } 
        return 0; 
    } 
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size) 
{ 
        struct sockaddr_storage ss; 
        unsigned long s = size; 
        ZeroMemory(&ss, sizeof(ss)); 
        ss.ss_family = af; 
        switch(af) { 
            case AF_INET: 
                ((struct sockaddr_in *)&ss)->sin_addr = *(struct in_addr *)src; 
                break; 
            case AF_INET6: 
                ((struct sockaddr_in6 *)&ss)->sin6_addr = *(struct in6_addr *)src; 
                break; 
            default: 
                return NULL; 
        } 
        /* cannot direclty use &size because of strict aliasing rules */ 
        return (WSAAddressToString((struct sockaddr *)&ss, sizeof(ss), NULL, dst, &s) == 0)? 
               dst : NULL; 
    } 
    int lookup_host () 
{ 
        struct addrinfo hints, *res; 
        int errcode; 
        char addrstr[100]; 
        void *ptr; 
        memset (&hints, 0, sizeof (hints)); 
        hints.ai_family = AF_INET6; 
        errcode = getaddrinfo ("IPv6test.ntes53.netease.com", NULL, &hints, &res); 
        if (errcode != 0) 
        { 
            perror ("getaddrinfo"); 
            printf("%d",errcode); 
            return -1; 
        } 
        while (res) 
        { 
            // inet_ntop (res->ai_family, res->ai_addr->sa_data, addrstr, 100); 
            sockaddr_in in1; 
            memcpy(&in1.sin_addr, res->ai_addr->sa_data, sizeof(res)); 
            switch (res->ai_family) 
            { 
                case AF_INET: 
                    ptr = &((struct sockaddr_in *) res->ai_addr)->sin_addr; 
                    break; 
                case AF_INET6: 
                    ptr = &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr; 
                    break; 
            } 
            inet_ntop(res->ai_family, ptr, addrstr, 100); 
            // sockaddr_in6 in; 
            // memcpy(∈.sin6_addr, ptr, sizeof(ptr)); 
            printf ("IPv%d address: %s (%s)\n", res->ai_family == PF_INET6 ? 6 : 4, 
                   addrstr, res->ai_canonname); 
            //printf ("IPv%d address: %s (%s)\n", res->ai_family == PF_INET6 ? 6 : 4, 
            //        inet_ntoa(in.sin6_addr), res->ai_canonname); 
            res = res->ai_next; 
        } 
        return 0; 
    } 
    int main (void) 
{ 
        printf("start\n"); 
        lookup_host(); 
} 
    } 

Python

  • socket.gethostbyname

import socket 
result = socket.gethostbyname("IPv6test.ntes53.netease.com") 
print result 
  • getaddrinfo
import socket 
result = socket.getaddrinfo("IPv6test.ntes53.netease.com", 0, socket.AF_INET6) 
print result 
result = socket.getaddrinfo("IPv6test.ntes53.netease.com", 0, socket.AF_INET) 
print result 
result = socket.getaddrinfo("IPv6test.ntes53.netease.com", 0, socket.AF_UNSPEC) 
print result 

當不指定 socktype 時,此值默認爲  socket.AF_UNSPEC

HTTP 請求

Python

requests 包

import requests 
response = requests.get("http://IPv6test.ntes53.netease.com:8000", stream=True) 
print response.raw._fp.fp._sock.getpeername() 

C++

#include <stdio.h> 
#include <curl/curl.h> 
 
int main(void) 
{ 
  CURL *curl; 
  CURLcode res; 
 
  curl = curl_easy_init(); 
  if(curl) { 
    curl_easy_setopt(curl, CURLOPT_URL, "http://IPv6test.ntes53.netease.com:8000"); 
    /* example.com is redirected, so we tell libcurl to follow redirection */  
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 
      // curl_easy_setopt(curl, CURL_IPRESOLVE_V6, 1L);  // 使用 IPv6 地址 
      // curl_easy_setopt(curl, CURL_IPRESOLVE_V4, 1L);  // 使用 IPv4 地址 
      // curl_easy_setopt(curl, CURL_IPRESOLVE_WHATEVER, 1L);  // 獲取系統允許的 IPv4 或者 IPv6 地址  
    /* Perform the request, res will get the return code */  
    res = curl_easy_perform(curl); 
    /* Check for errors */  
    if(res != CURLE_OK) 
      fprintf(stderr, "curl_easy_perform() failed: %s\n", 
              curl_easy_strerror(res)); 
 
    /* always cleanup */  
    curl_easy_cleanup(curl); 
  } 
  return 0; 
} 
相關文章