一份完整的 IPv6 環境下 DNS 相關測試
摘要:假定用戶只有 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)
-
此算法的簡略工作流程如下:
-
當客戶端是雙棧環境時,客戶端會向緩存 DNS 服務器發起域名 A 記錄與 AAAA 記錄的解析請求,並受到解析結果,對應下圖中的 1-4
-
客戶端獲取到解析地址後,會同時使用 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; }