董涛,网易游戏高级运维工程师,主要工作方向为网易集团 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
- 1.
-
AAAA 记录
AAAA 是一个域名指向 IPv6 地址的解析结果。如果想要一个域名解析到 IPv6 地址,则需要设置此种类型的解析结果。同一个域名可以同时有 A 与 AAAA 两种记录类型, 例如 ipv6test.ntes53.netease.com. 1800 IN AAAA 2403:c80:100:3000::7b3a:a646
- 1.
-
缓存 DNS 服务器
用户直接使用的 DNS 服务器,各种平台、操作系统上直接设置的 DNS 服务器,常见的有 8.8.8.8, 114.114.114.114
- 1.
-
权威 DNS 服务器
用于域名的管理。权威 DNS 服务器只对自己所拥有的域名进行域名解析,对于不是自己的域名则拒绝应答。例如网易的权威 DNS 服务器只会响应网易域名的请求,对于其他域名,则拒绝应答。
- 1.
-
双栈网络环境
双栈网络环境即客户端或服务器同时拥有 IPv4、IPv6 两种网络环境,可以简单的理解为机器上既有 IPv4 地址又有 IPv6 地址
- 1.
测试场景
下文中所有测试使用的程序均为测试方法中的程序
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;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
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();
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
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();
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
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(&in.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();
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
Python
-
socket.gethostbyname
import socket
result = socket.gethostbyname("IPv6test.ntes53.netease.com")
print result
- 1.
- 2.
- 3.
- 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
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
当不指定 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()
- 1.
- 2.
- 3.
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;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.