一份完整的IPv6环境下DNS相关测试

董涛,网易游戏高级运维工程师,主要工作方向为网易集团 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)规模部署行动计划》(

推动各单位加快支持 IPv6。

IPv6 支持度报告的数据来源是:下一代国家互联网中心在 2017 年 11 月发布的 IPv6 支持度报告(, 感兴趣的同学可以查看原文。

二、IPv6 环境下 DNS 相关测试

背景介绍

名词简介

A 记录

A 记录是一个域名指向 IPv4 地址的解析结果,即最常见的记录类型, 例如ipv6test.ntes53.netease.com. 1800 INA 123.58.166.70

AAAA 记录

AAAA 是一个域名指向 IPv6 地址的解析结果。如果想要一个域名解析到 IPv6 地址,则需要设置此种类型的解析结果。同一个域名可以同时有 A 与 AAAA 两种记录类型, 例如ipv6test.ntes53.netease.com. 1800 INAAAA2403: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?(?forum=w7itpronetworking)

目前广泛使用的 IPv4 与 IPv6 优先选择算法为 Happy Eyeballs(:

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

此算法会优先选择 IPv6 链路使用

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

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

当客户端是双栈环境时,客户端会向缓存 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(&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();}}Python

socket.gethostbyname

import socketresult = socket.gethostbyname("IPv6test.ntes53.netease.com")print result

getaddrinfo

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

当不指定 socktype 时,此值默认为 socket.AF_UNSPEC。

HTTP 请求

Python

requests 包

import requestsresponse = requests.get(":8000", stream=True)print response.raw._fp.fp._sock.getpeername()

C++

libcurl

#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, ":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;}

来源:网易游戏运维平台 

原文链接:IPv6 支持度报告和 IPv6 环境下 DNS 相关测试

2019 年11月 GOPS 2019 · 上海站,《深入理解 Nginx:模块开发及架构解析》作者、杭州智链达数据有限公司联合创始人及 CTO 将带来 HTTP 性能极限调优的精彩演讲,敬请期待。

更多 GOPS 精彩预告,点击阅读原文