摘要:假定用户只有 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; 
} 
相关文章