IPv6 socket编程上--原理篇

时间:2022-04-25
本文章向大家介绍IPv6 socket编程上--原理篇,主要内容包括背景、问题复杂性、不同IP stack组合的处理方式、v6 ip + IPv4-only、v6 ip + IPv6-only or IPv4-IPv6、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

背景

研究IPv6 socket编程原因:

Supporting IPv6 in iOS 9 WWDC2015苹果宣布在ios9支持纯IPv6的网络服务,并且要求2016年提交到app store的应用必须兼容纯IPv6的网络,要求适配的系统版本是ios9以上(包括ios9)。

写这篇文章虽然是来源于iOS的需求,但是下面的内容除了特别说明外,大部分都适用于其他平台。 IPv6的复杂度之一,在于和IPv4的兼容和相互访问。本文会提及其他的互相访问技术,但是重点是NAT64,也是一般手机用户最有可能遇到的纯IPv6环境。 本文重点在不同IP stack组合的处理方式判断客户端支持的IP stack

问题复杂性

为了降低问题的复杂性,我们先把v4 socket排除掉,统一使用v6 socket。 v6 socket的区别是使用AF_INET6来创建。 IPv6转换机制有很多种,苹果期望iOS app能兼容NAT64/DNS64的方式,因此其他方式我们先不考虑。

  1. socket api支持 RFC 4038 - Application Aspects of IPv6 Transition
    • v4 socket接口只能支持IPv4 stack
    • v6 socket能支持IPv4 stack和IPv6 stack
  2. 服务器IP
    • 返回v4 IP
    • 返回v6 IP
  3. 用户本地IP stack
    • IPv4-only
    • IPv6-only
    • IPv4-IPv6 Dual stack
  4. 各种IPv6转换机制
    • NAT64/DNS64 64:ff9b::/96用于v6的本地网络通过NAT访问v4的资源。RFC 6146RFC 6147
    • 6to4 2002::/16用于两个拥有v4公网地址的IPv6 only子网的互相访问。RFC 6343
    • Teredo tunneling 2001::/32通过隧道的方式让两个IPv6 only子网互相访问,没有NAT问题。RFC 4380
    • 464XLAT 用于程序只有v4地址(使用v4 socket),但是本地网络是ipv6网络,程序需要访问v4资源,类似NAT64,不过区别在于服务器是运营商提供,手机上需要安装CLAT服务RFC 6877
    • 还有很多兼容方案,复杂程度都很高,这里不介绍了

不同IP stack组合的处理方式

v4 ip + IPv4-only or IPv4-IPv6 Dual stack

在这样的情况下我们虽然用的是v6的socket,但是必须要让socket走的是v4的协议。 这里,让我们先了解下IPv6的保留地址(类似IPv4,192.168.., 127.*..这种)这里假设读者已经对IPv6地址组成和书写方式有一定了解的了解。

local_addr.sin6_addr, v4mapped_str_local_addr, 64);//IPv4-mapped IPv6 address sample
//address init 
const char* ipv4mapped_str ="::FFFF:14.17.32.211";
in6_addr ipv4mapped_addr = {0};
int v4mapped_r = inet_pton(AF_INET6, ipv4mapped_str, &ipv4mapped_addr);

sockaddr_in6 v4mapped_addr = {0};
v4mapped_addr.sin6_family = AF_INET6;
v4mapped_addr.sin6_port = htons(80);
v4mapped_addr.sin6_addr = ipv4mapped_addr;

//socket connect
int v4mapped_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); 
std::string v4mapped_error;
if (0 != connect(v4mapped_sock, (sockaddr*)&v4mapped_addr, 28))
{
    v4mapped_error = strerror(errno);
}

//get local ip
sockaddr_in6 v4mapped_local_addr = {0};
socklen_t v4mapped_local_addr_len = 28;
char v4mapped_str_local_addr[64] = {0};
getsockname(v4mapped_sock, (sockaddr*)&v4mapped_local_addr, &v4mapped_local_addr_len);
inet_ntop(v4mapped_local_addr.sin6_family, &v4mapped_close(v4mapped_sock);

从上文可以看到如果服务器地址为128.0.0.128,我们转换成IPv4-mapped IPv6 address::ffff:128.0.0.128或者纯16进制::ffff:ff00:00ff, 然后赋值给sockaddr_in6.sin6_addr = "::ffff:128.0.0.128";(注意这里是伪代码,真正代码还要用inet_pton进行转换)。这个socket虽然用了IPv6的sockaddr_in6,但实际上走的是IPv4 stack。

IPv4-mapped IPv6 address是让用户能够使用一致的socket api,来访问IPv4和IPv6网络。

上文提及RFC 4038 - Application Aspects of IPv6 Transition对这种情况进行说明。

local_addr.sin6_addr, v4mapped_str_local_addr, 64);//IPv4-mapped IPv6 address sample
//address init 
const char* ipv4mapped_str ="::FFFF:14.17.32.211";
in6_addr ipv4mapped_addr = {0};
int v4mapped_r = inet_pton(AF_INET6, ipv4mapped_str, &ipv4mapped_addr);

sockaddr_in6 v4mapped_addr = {0};
v4mapped_addr.sin6_family = AF_INET6;
v4mapped_addr.sin6_port = htons(80);
v4mapped_addr.sin6_addr = ipv4mapped_addr;

//socket connect
int v4mapped_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); 
std::string v4mapped_error;
if (0 != connect(v4mapped_sock, (sockaddr*)&v4mapped_addr, 28))
{
    v4mapped_error = strerror(errno);
}

//get local ip
sockaddr_in6 v4mapped_local_addr = {0};
socklen_t v4mapped_local_addr_len = 28;
char v4mapped_str_local_addr[64] = {0};
getsockname(v4mapped_sock, (sockaddr*)&v4mapped_local_addr, &v4mapped_local_addr_len);
inet_ntop(v4mapped_local_addr.sin6_family, &v4mapped_close(v4mapped_sock);

v4 ip + IPv6-only

这里是重点,也是苹果要求支持的主要场景。这里会涉及到NAT64/DNS64,关于这个环境的搭建请参考Supporting IPv6 DNS64/NAT64 Networks(废弃了的SIIT技术我们就不讨论了)

这里我们先看看wikipedia对NAT64/DNS64的描述。

NAT64 is a mechanism to allow IPv6 hosts to communicate with IPv4 servers. The NAT64 server is the endpoint for at least one IPv4 address and an IPv6 network segment of 32-bits, e.g., 64:ff9b::/96 (RFC 6052, RFC 6146). The IPv6 client embeds the IPv4 address with which it wishes to communicate using these bits, and sends its packets to the resulting address. The NAT64 server then creates a NAT-mapping between the IPv6 and the IPv4 address, allowing them to communicate.[^2]

DNS64 describes a DNS server that when asked for a domain's AAAA records, but only finds A records, synthesizes the AAAA records from the A records. The first part of the synthesized IPv6 address points to an IPv6/IPv4 translator and the second part embeds the IPv4 address from the A record. The translator in question is usually a NAT64 server. The standard-track specification of DNS64 is in RFC 6147.

There are two noticeable issues with this transition mechanism:

It only works for cases where DNS is used to find the remote host address, if IPv4 literals are used the DNS64 server will never be involved.

Because the DNS64 server needs to return records not specified by the domain owner, DNSSEC validation against the root will fail in cases where the DNS server doing the translation is not the domain owner's server.[^3]

这里大概描述一下NAT64的工作流程,首先局域网内有一个NAT64的路由设备并且有DNS64的服务。

  1. 客户端进行getaddrinfo的域名解析.
  2. DNS返回结果,如果返回的IP里面只有v4地址,并且当前网络是IPv6-only网络,DNS64服务器会把v4地址加上64:ff9b::/96的前缀,例如64:ff9b::14.17.32.211。如果当前网络是IPv4-only或IPv4-IPv6,DNS64不会做任何事情。
  3. 客户端拿到IPv6的地址进行connect
  4. 路由器发现地址的前缀为64:ff9b::/96,知道这个是NAT64的映射,是需要访问14.17.32.211。这个时候进行需要NAT64映射,因为到外网需要转换成IPv4 stack。
  5. 当数据返回的时候,按照NAT映射,IPv4回包重新加上前缀64:ff9b::/96,然后返回给客户端。

apple的文档里面也有很详细的描述:

//NAT64 address sample
//address init
const char* ipv6_str ="64:ff9b::14.17.32.211";
in6_addr ipv6_addr = {0};
int v6_r = inet_pton(AF_INET6, ipv6_str, &ipv6_addr);
sockaddr_in6 v6_addr = {0};
v6_addr.sin6_family = AF_INET6;
v6_addr.sin6_port = htons(80);
v6_addr.sin6_addr = ipv6_addr;

//socket connect
int v6_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
std::string v6_error;
if (0 != connect(v6_sock, (sockaddr*)&v6_addr, 28))
{
    v6_error = strerror(errno);
}

//get local ip
sockaddr_in6 v6_local_addr = {0};
socklen_t v6_local_addr_len = 28;
char v6_str_local_addr[64] = {0};
getpeername(v6_sock, (sockaddr*)&v6_local_addr, &v6_local_addr_len);
inet_ntop(v6_local_addr.sin6_family, &v6_local_addr.sin6_addr, v6_str_local_addr, 64);

close(v6_sock);

这里讨论下比较坑的地方,按照NAT64的规则,客户端如果没有做DNS域名解析的话(微信依赖的是自己实现的NEWDNS),客户端就需要完成DNS64的工作。这里的关键点是,发现网络是IPv6-only的NAT64网络的情况下,我们可以自己补充上前缀64:ff9b::/96,然后进行正常的访问。然而这里客户端能获取的信息量一般都是很有限的,怎么样处理这个问题,后面有专门的章节来处理这个问题(判断客户端支持的IP stack)。

v6 ip + IPv4-only

这里一般connect的时候会返回错误码network is unreachable,因为根本没有v6的协议栈,就像没有硬件设备一样,但是不排除会有系统会返回no route to host。 当然,如果服务器的地址是 Teredo tunneling 2001::/32,可以客户端直接做隧道。如果是6to4 2002::/16,并且客户端有RAW socket权限加上非NAT网络,这种情况下可以客户端自己做6to4的路由。(这里的结论不一定百分百正确,还需要继续研读RFC)。

v6 ip + IPv6-only or IPv4-IPv6

这里只要没有配置上,是可以直接通讯的。 当然这里会涉及到一个问题,如果DNS返回上文说的6to4Teredo tunnelingpure native IPv6 addresses,这样的情况下我们怎么样做IP的选择呢,这个可以参照RFC 3484 - Default Address Selection for Internet Protocol version 6 (IPv6)

以上是IPv6 socket编程的基本原理,鉴于篇幅关系,具体的实现方案将在下篇详细说明。