IPv6出来已经很多年,虽然距离普及还很远,但项目里要加上,有没有人用是一码事,但不加上肯定过不了审。IPv6最大的问题是包格式与IPv4不兼容

IPv4包格式

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

IPv6包格式

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version| Traffic Class |           Flow Label                  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Payload Length        |  Next Header  |   Hop Limit   |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   +                                                               +
   |                                                               |
   +                         Source Address                        +
   |                                                               |
   +                                                               +
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   +                                                               +
   |                                                               |
   +                      Destination Address                      +
   |                                                               |
   +                                                               +
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

作为一个程序员,粗略一看,这个应该是兼容的啊。毕竟前4bit都是版本号,拿到数据包时,判断一下版本号,根据不同的版本号做不同的处理,即可做兼容,很多软件都是这么做的。然而问题是,IP数据包涉及的不仅仅是软件,还有硬件,比如路由器。当一个数据包经过路由器时,它需要解析包头里的数据,得到目标地址,才知道数据包转发到哪里。如果这个包头格式不一样了,那这个数据包就无法正常转发。对于软件,可以重新发布一个版本来解决,但绝大部分的家用路由器,是不带更新功能的,那要让它支持Ipv6,只能扔掉买一个新的。在现实生活中,一个数据包的传输,可能会经过很多路由的,例如

客户端 --------> 家庭路由 --------> 小区路由 --------> ISP主路由 --------> 服务器

当这个问题扩大到整个社会,就会有无数的家庭路由,无数的小区路由。ISP的主路由和服务器由于商业的驱动,可以及时更新,而无数的家庭路由,无数的小区路由,显然没法短时间内全部更新,也没必要更新。

目前的情况是,客户端、路由器都有可能支持IPv6,也有可能都不支持,因此IPv4和IPv6的兼容是必须得做的,不然就可能失去一部分用户了。这个兼容是指“不管客户端以及所经过的路由是IPv4还是IPv6,不管服务器是IPv6还是IPv4,两者都能正常进行交互”。排除正常的条件((如:客户端为IPv6,服务器也是IPv6)的情况,需要特殊处理的情况为IPv4访问IPv6IPv6访问IPv4

NAT64 & DNS64

既然IPv6和IPv4兼容性是由它们的IP报文不一样,那么可以弄一个专门转换报文的服务,即NAT64(Network Address Translation IPv6 to IPv4)。而一个IPv6客户端想把一个报文发往一个IPv4服务器,就需要一个包含IPv4地址的IPv6地址,这由一个特殊的DNS服务提供,即DNS64

假如现在有一台IPv4的服务器,因无人维护无法升级到IPv6,但用户已更换了新手机,新路由器,升级到了IPv6。那这时候需要给这台服务器配置NAT64和DNS64,否则用户就访问不了了。

  1. 运维部署一台NAT64服务器(自建,也可以是云服务商提供的服务器),配置好服务器的IPv4地址(以192.168.0.100为例)和IPv6前缀(一般是64:ff9b::/96 )
  2. 运维在DNS64服务器(DNS64服务器可能是自建,也可能是公共DNS64服务器,也可以是云服务商提供的服务器)把域名指向NAT64服务器的IPv4地址(以192.168.0.101为例)及前缀(必须和NAT64配置的前缀一致)
  3. IPv6客户端需要和服务器通信,根据域名发起IPv6 DNS查询,由于服务器不支持IPv6,所以没有查询到。但是DNS64服务器发现了一个IPv4的地址,于是把这个IPv4加上前缀,得到一个IPv6地址,即64:ff9b::192.168.0.101,于是客户端往这个地址发请求。
  4. NAT64收到IPv6数据包,会解析IP地址的前缀,发现和运维配置的前缀一致,于是把这个数据包转换为一个IPv4数据包,按NAT规则往配置好的服务器地址发送IPv4数据包
  5. IPv4服务器收到IPv4数据包,返回IPv4数据包
  6. NAT64收到服务器返回的数据包,按NAT规则转换为IPv6数据包,返回给IPv6客户端,完成了交互
  DNS64
  ^   |
  |   |
  |   v
IPv6客户端 --------> IPv6路由器 --------> NAT64(192.168.0.101) --------> IPv4服务器(192.168.0.100)

NAT64原本设计的目的是让IPv6客户端访问IPv4,而不是IPv4访问IPv6。因为IPv4是旧标准,既然服务器支持IPv6,说明是从IPv4升级而来的,那保留IPv4功能即可,一般用不着NAT转换。但事情也不是这么绝对,因此虽然很少有人提及,但还是有NAT46这种东西的,见:NAT64 - NAT46

  1. 运维部署NAT46服务器
  2. 运维在DNS46把域名指向NAT46服务器(NAT46服务器最好有一个公网IPv4地址,没有的话据说DNS46会做一个临时mapping,我觉得是个大问题,除非部署在私有网络)
  3. IPv4客户端查询域名得到一个IPv4地址,往这个地址发一个IPv4数据包
  4. NAT46服务器收到一个IPv4数据包,把这个IPv4的地址加上一个前缀得到IPv6地址,然后按NAT规则往服务器发送IPv6数据包
  5. IPv6服务器收到IPv6数据包,正常返回IPv6数据包
  6. NAT46服务器收到返回的IPv6数据包,转换为IPv4数据往,返回给IPv4客户端
   DNS
  ^   |
  |   |
  |   v
IPv4客户端 --------> IPv4路由器 --------> NAT46(192.168.0.101) --------> IPv6服务器

可以看到,NAT46其实是NAT64反过来的,实际上它和NAT64服务器回包的流程基本一致,因此大部分NAT64服务配置一下,都能支持NAT46,比如NE40E

而对于DNS,虽然有纯IPv4的DNS,也有纯IPv6的DNS,但是一般都会升级为DNS64,即可支持IPv4的域名查询,也可以支持IPv6的查询,例如114.114.114.114是一个很常见的IPv4 DNS服务器,但也可以返回IPv6的地址

~$ nslookup -type=AAAA www.google.com 114.114.114.114
Server:		114.114.114.114
Address:	114.114.114.114#53

Non-authoritative answer:
Name:	www.google.com
Address: 2001::4a7d:278a

NAT64是在运维层面去解决兼容性,不需要程序去做兼容。但是部署NAT64是需要成本的,你可以买硬件,也可以从云服务那里买整套服务,但这都是白花花的银子啊。对于大部分公司而言,他们的产品是有程序员去维护,可以从程序做兼容,而不需要花这一笔钱。

Dual Stack

从另一个角度来看,既然IPv4和IPv6不兼容,那就没必要让它们兼容。可以同时开启IPv4和IPv6,这样各走各的路,互不干扰。那这就需要同时实现IPv4和IPv6协议栈,即双栈(Dual Stack)。

现在新的路由和和操作系统基本都是支持双栈的,例如ubuntu 20.04下通过ip a可以查看

~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp2s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether b4:b5:2f:91:fe:21 brd ff:ff:ff:ff:ff:ff
    inet 192.168.3.6/24 brd 192.168.3.255 scope global dynamic noprefixroute enp2s0
       valid_lft 73235sec preferred_lft 73235sec
    inet6 fe80::f2a1:25a2:d365:468c/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

可以看到,网卡2有两个地址:inet 192.168.3.6inet6 fe80::f2a1:25a2:d365:468c,说明支持双栈。不过这里不打算细说这些设备,作为一个程序员更关注的是怎么实现双栈程序。

最简单的办法,同时开两个socket,一个IPv4,一个IPv6,问题是这样写代码有点复杂。在IPv6出来的时候,在系统的底层库提供了双栈的接口,直接调用就可以了。

  • 服务端
// 以IPv6进行监听
int fd = ::socket(AF_INET6, SOCK_STREAM, IPPROTO_IP);

// 把ipv6 only关掉,这样会同时开启两个监听,一个IPv4,一个IPv6
// 允许v4的连接以 IPv4-mapped IPv6 的形式连进来
int optval = 0;
if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&optval, sizeof(optval)) < 0)
{
    return -1;
}

struct sockaddr_in6 sk_socket;
memset(&sk_socket, 0, sizeof(sk_socket));
sk_socket.sin6_family = AF_INET6;
sk_socket.sin6_port   = htons(port);

// 使用新的接口inet_pton,它即支持IPv4,也支持IPv6,所以host可以传::ffff:127.0.0.1,
// 也可以传fe80::f2a1:25a2:d365:468c,但不能直接传127.0.0.1,可以用getaddinfo转换
if (inet_pton(AF_INET6, host, &sk_socket.sin6_addr) < 0)
{
    return -1;
}

if (::bind(fd, (struct sockaddr *)&sk_socket, sizeof(sk_socket)) < 0)
{
    return -1;
}

if (::listen(fd, 256) < 0)
{
    return -1;
}

return 0;
  • 客户端
// 创建IPv6连接
int fd = ::socket(AF_INET6, SOCK_STREAM, IPPROTO_IP);

struct sockaddr_in6 sk_socket;
memset(&sk_socket, 0, sizeof(sk_socket));
sk_socket.sin6_family = AF_INET6;
sk_socket.sin6_port   = htons(port);

// 使用新的接口inet_pton,host可以传::ffff:127.0.0.1,
// 也可以传fe80::f2a1:25a2:d365:468c,但不能直接传127.0.0.1,可以用getaddinfo转换
if (inet_pton(AF_INET6, host, &sk_socket.sin6_addr) < 0)
{
    return -1;
}

if (::connect(fd, (struct sockaddr *)&sk_socket, sizeof(sk_socket)) < 0)
{
    return -1;
}

return 0;
  • DNS查询
int32 Socket::get_addr_info(std::vector<std::string> &addrs, const char *host)
{
    struct addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_INET6;
    hints.ai_socktype = SOCK_STREAM;

    hints.ai_flags = AI_V4MAPPED; // 目标无ipv6地址时,返回v4-map-v6地址

    hints.ai_protocol  = 0; /* Any protocol */
    hints.ai_canonname = NULL;
    hints.ai_addr      = NULL;
    hints.ai_next      = NULL;

    struct addrinfo *result;
    if (0 != getaddrinfo(host, nullptr, &hints, &result))
    {
        return -1;
    }

    char buf[INET6_ADDRSTRLEN];
    for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next)
    {
        if (inet_ntop(rp->ai_family,
                      &((struct sockaddr_in6 *)rp->ai_addr)->sin6_addr, buf,
                      sizeof(buf)))
        {
            addrs.emplace_back(buf);
        }
        else
        {
            return -1;
        }
    }
    freeaddrinfo(result);

    return 0;
}

上面的代码是从我的工程里拷贝而来,不一定能直接运行,仅供参考,详见原文件socket.cpp。执行服务端的程序后,可以用telnet测试效果

root@debian:/home# netstat -lp | grep master
tcp6       0      0 [::]:8182               [::]:*                  LISTEN      1458/./master
root@debian:/home# telnet 127.0.0.1 8182
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^C^]
telnet> quit
Connection closed.
root@debian:/home# telnet -6 ::1 8182
Trying ::1...
Connected to ::1.
Escape character is '^]'.
^]
telnet> quit
Connection closed.
root@debian:/home#

PS

  1. ::1去监听时,只能通过::1来连接,用127.0.0.1是不行的。用::监听则可以用::1或者127.0.0.1连接

  2. Linux下,同一个程序以Dual Stack模式监听时,netstat只显示IPv6的监听(上面telnet测试就只grep到一个记录),而部分程序(如sshd)则显示两个,猜测可能是开了两个socket监听而不是用双栈实现。而Win下同一个程序则是显示两个记录。

# linux
root@debian:/home# netstat -lp | grep sshd
tcp        0      0 0.0.0.0:ssh             0.0.0.0:*               LISTEN      595/sshd
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN      595/sshd

# win
netstat -aob
活动连接

  协议  本地地址          外部地址        状态           PID
  TCP    0.0.0.0:27015          DESKTOP-KR5SLB1:0      LISTENING       7812
 [winipv6.exe]
  TCP    [::]:27015             DESKTOP-KR5SLB1:0      LISTENING       7812
 [winipv6.exe]

Linux的这个设定一度让我以为我的程序没有监听成功,查了好久Bug。也有人遇到同样的问题,不过官方没有确认为bug,见https://bugs.launchpad.net/ubuntu/+source/net-tools/+bug/657270

  1. 双栈虽然可以接受IPv4连接,但实际上是以IPv6模式来处理的,因此获得到的IP地址为::ffff:192.0.2.1这种IPv4-mapped IPv6的格式,而不是原生的IPv4地址

  2. ::ffff:127.0.0.1虽然是一个IPv6地址,但它不是::1,而是是等同于127.0.0.1。即监听::ffff:127.0.0.1时,只能从127.0.0.1连接,用::1是连不上的。如果开启IPV6_V6ONLY,无法监听这个地址的,在win下抛出一个1049错误。我猜测IPv6这个协议段都是用来做兼容的,原生的IPv6不使用这个号段。

  3. ::ffff:c000:201::ffff:192.0.2.1这两个地址是相同,把::ffff:c000:201用getaddrinfo解析,得到的就是::ffff:192.0.2.1,它们的转换算法:

IP 192.0.2.1 的十六进制转换为0xC0000201
192 十六进制是 c0
0   十六进制是 00
2   十六进制是 02
1   十六进制是 01

可以在线直接转换:IPv6 to IPv4 IPv4 to IPv6

客户端的连接处理

服务端如果需要对外部署的话,无论是自建服务器还是云服务器,在部署的时候都能够知道是否支持IPv6,但客户端并是不能确定的。公司可以发布一个支持IPv6的客户端,但即使查询IPv6的DNS成功,也无法保证客户端到服务器之间的所有设备都支持IPv6,DNS查询成功只是表示客户端到DNS服务器之间的IPv6链路是通的,与服务器不是同一条链路,直接用IPv6去连服务器可能会失败。IETF推荐的做法是:优先查询IPv6地址,然后是IPv4地址,一个个尝试去连接。没看错,就是for循环一个个去试。

域名检测

想测试一个域名是否支持IPv6,有很多工具,比如nslookup -type=AAAA www.google.com 114.114.114.114,也可以在线测试,例如https://www.boce.com/ipv6/

posted on 2020-11-20 23:15  coding my life  阅读(7314)  评论(0编辑  收藏  举报