研究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的方式,因此其他方式我们先不考虑。
64:ff9b::/96
用于v6的本地网络通过NAT访问v4的资源。RFC 6146 、RFC 61472002::/16
用于两个拥有v4公网地址的IPv6 only子网的互相访问。RFC 63432001::/32
通过隧道的方式让两个IPv6 only子网互相访问,没有NAT问题。RFC 4380CLAT服务
。RFC 6877在这样的情况下我们虽然用的是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的服务。
64:ff9b::/96
的前缀,例如64:ff9b::14.17.32.211
。如果当前网络是IPv4-only或IPv4-IPv6,DNS64不会做任何事情。64:ff9b::/96
,知道这个是NAT64的映射,是需要访问14.17.32.211
。这个时候进行需要NAT64映射,因为到外网需要转换成IPv4 stack。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)。
这里一般connect的时候会返回错误码network is unreachable
,因为根本没有v6的协议栈,就像没有硬件设备一样,但是不排除会有系统会返回no route to host
。 当然,如果服务器的地址是 Teredo tunneling 2001::/32
,可以客户端直接做隧道。如果是6to4 2002::/16
,并且客户端有RAW socket权限加上非NAT网络,这种情况下可以客户端自己做6to4的路由。(这里的结论不一定百分百正确,还需要继续研读RFC)。
这里只要没有配置上,是可以直接通讯的。 当然这里会涉及到一个问题,如果DNS返回上文说的6to4
或Teredo tunneling
或pure native IPv6 addresses
,这样的情况下我们怎么样做IP的选择呢,这个可以参照RFC 3484 - Default Address Selection for Internet Protocol version 6 (IPv6)。
以上是IPv6 socket编程的基本原理,鉴于篇幅关系,具体的实现方案将在下篇详细说明。