未来数年内,因特网也许会逐渐从IPv4过渡到IPv6,在过渡阶段,基于IPv4的现有应用能与基于IPv6的全新应用协同工作非常重要。例如,厂商不应只提供仅能与IPv6 telnet服务器程序协同工作的telnet客户程序,而应该提供既能与IPv4服务器协同工作的客户程序,又能与IPv6服务器协同工作的客户程序。更理想的情形是,一个IPv6或IPv4 telnet客户程序既能与IPv4服务器程序协同工作,也能与IPv6服务器协同工作。
本章假设主机运行着双栈,即一个IPv4协议栈和一个IPv6协议栈。在向IPv6转换的漫长过渡期内,主机和路由器会这样运行很多年。某个时间点后,许多系统可以关闭它们的IPv4协议栈,但只有时间才能告诉我们这种情况何时(以及是否)会发生。
双栈主机的一个基本特性是其上的IPv6服务器既能处理IPv4客户,又能处理IPv6客户,这是通过IPv4映射的IPv6地址实现的:
上图中,左侧有一个IPv4客户和一个IPv6客户,右侧的服务器程序使用IPv6编写,该服务器创建了一个绑定在IPv6通配地址和TCP端口9999上的IPv6监听TCP套接字。
上图我们假设客户和服务器主机处于同一个以太网,它们也能通过路由器连接,只要通过的所有路由器都同时支持IPv4和IPv6,但这对我们的讨论没有影响。
我们假设这两个客户都发送SYN分节建立与服务器的连接。IPv4客户主机在一个IPv4数据报中载送SYN,IPv6客户主机在一个IPv6数据报中载送SYN。
来自IPv4客户的TCP分节在以太网线上表现为一个以太网首部后跟一个IPv4首部、一个TCP首部、TCP数据,以太网首部中包含的类型字段值为0x0800,即把本以太网帧标识为一个IPv4帧,TCP首部中包含的目的端口为9999,IPv4首部中包含的IP地址为206.62.226.42。
来自IPv6客户的TCP分节在以太网线上表现为一个以太网首部后跟一个IPv6首部、一个TCP首部、TCP数据。以太网首部中的类型字段值为0x86dd,即把本以太网帧标识为一个IPv6帧。这个TCP首部和IPv6数据报中的TCP首部格式完全一样,也包含值为9999的目的端口。IPv6首部中包含的目的IP地址为5f1b:df00:ce3e:e200:20:800:2b37:6426。
接收数据链路通过查看以太网类型字段把每个帧传递给相应的IP模块。IPv4模块结合其上的TCP模块检测到IPv4数据报的目的端口对应一个IPv6套接字,于是把该数据报IPv4首部中的源IPv4地址转换成一个等价的IPv4地址映射的IPv6地址。当accept系统调用把这个已经接受的IPv4客户连接返回给服务器进程时,这个映射后的地址将作为客户的IPv6地址返回到服务器的IPv6套接字。该连接上的其余数据报同样都是IPv4数据报。
当accept系统调用把接受的IPv6客户连接返回给服务器进程时,客户的IPv6地址就是IPv6首部中的源地址,该连接上其余数据报都是IPv6数据报。
总结以上一个IPv4客户和一个IPv6服务器通信的步骤:
1.IPv6服务器启动后创建一个IPv6监听套接字,假定服务器把通配地址捆绑到该套接字。
2.IPv4客户调用gethostbyname找到服务器主机的一个A记录。服务器主机既有一个A记录,又有一个AAAA记录,因为它同时支持IPv4和IPv6,但IPv4客户只需要A记录。
3.客户调用connect,导致客户主机发送一个IPv4 SYN到服务器。
4.服务器主机接收这个目的地为IPv6监听套接字的IPv4 SYN,设置一个标志指示本连接应使用IPv4映射的IPv6地址,然后响应一个IPv4 SYN/ACK。该连接建立后,由accept函数返回给服务器进程的地址就是这个IPv4映射的IPv6地址。
5.当服务器主机往这个IPv4映射的IPv6地址发送TCP分节时,其IP栈产生目的地址为该IPv6地址对应的IPv4地址,因此客户和服务器之间都使用IPv4载送数据报。
6.除非服务器显式检查这个IPv6地址是不是一个IPv4映射的IPv6地址(可用IN6_IS_ADDR_V4MAPPED宏检查),否则它永远不知道自己是在与一个IPv4客户通信。这个细节由双协议栈处理,同样地,IPv4客户也不知道自己是在与一个IPv6服务器通信。
IPv6的UDP服务器也类似。
对上图的解释:
1.如果收到一个目的地为某个IPv4套接字的IPv4数据报,则无需任何特殊处理。它们是上图标有IPv4的两个箭头,一个到TCP,一个到UDP。服务器和客户之间交换的是IPv4数据报。
2.如果收到一个目的地为某个IPv6套接字的IPv6数据报,则无需任何特殊处理。它们是上图标有IPv6的两个箭头,一个到TCP,一个到UDP。服务器和客户之间交换的是IPv6数据报。
3.如果收到一个目的地为某个IPv6套接字的IPv4数据报,则内核把该数据报的源IPv4地址映射的IPv6地址作为accept(TCP)或recvfrom(UDP)函数返回的对端IPv6地址。它们是上图中的两个虚线箭头。任何一个IPv4地址总能表示成一个IPv6地址。客户和服务器之间交换的是IPv4数据报。
4.一般,一个IPv6地址无法表示成一个IPv4地址,因此图中没有从IPv6协议框到IPv4套接字的箭头。
大多双栈主机使用以下规则处理监听套接字:
1.IPv4监听套接字只接受来自IPv4客户的外来连接。
2.如果服务器有绑定了通配地址的IPv6监听套接字,且该套接字没有设置IPV6_V6ONLY套接字选项,则该套接字既能接受来自IPv4客户的外来连接,又能接受来自IPv6客户的外来连接。对于来自IPv4客户的连接,其服务器端的对端IP地址是客户IPv4地址映射的IPv6地址。
3.如果服务器有一个IPv6监听套接字,且绑定在其上的是除IPv4映射的IPv6地址之外的某个非通配IPv6地址,或另一种情况,即绑定在其上的是IPv6通配地址,但设置了IPV6_V6ONLY套接字选项,则该套接字只能接受来自IPv6客户的外来连接。
对换一下上例中的客户和服务器使用的协议,考虑运行在双栈主机上的IPv6的TCP客户和运行在只支持IPv4上的TCP服务器:
1.一个IPv4服务器在只支持IPv4的一个主机上启动后创建一个IPv4监听套接字。
2.IPv6客户启动后调用getaddrinfo查找IPv6地址(通过请求AF_INET6地址,在hints结构中设置了AI_V4MAPPED标志)。由于只支持IPv4的那个服务器主机只有A记录,因此返回给客户的是一个IPv4映射的IPv6地址。
3.IPv6客户在IPv6套接字地址结构中设置这个IPv4映射的IPv6地址,然后用该地址结构调用connect。内核检测到此地址是IPv4映射的IPv6地址后自动发送一个IPv4 SYN到服务器。
4.服务器响应一个IPv4 SYN/ACK,连接于是通过使用IPv4数据报建立。
用下图汇总以上通信步骤:
对于上图的解释:
1.如果一个IPv4的TCP客户指定一个IPv4地址以调用connect,或一个IPv4的UDP客户指定一个IPv4地址以调用sendto,则无需任何处理。这指的是图中标有IPv4的两个箭头。
2.如果一个IPv6的TCP客户指定一个IPv6地址以调用connect,或一个IPv6的UDP客户指定一个IPv6地址以调用sendto,则无需任何处理。这指的是图中标有IPv6的两个箭头。
3.如果一个IPv6的TCP客户指定一个IPv4映射的IPv6地址以调用connect,或一个IPv6的UDP客户指定一个IPv4映射的IPv6地址以调用sendto,则内核检测到这个映射地址后,改为发送一个IPv4数据报而非IPv6数据报。这指的是图中两个虚线箭头。
4.不论是调用connect还是sendto,IPv4客户都不能指定一个IPv6地址,因为16字节的IPv6地址超出了IPv4的sockaddr_in结构中in_addr成员结构的4字节长度。因此上图中没有从IPv4套接字到IPv6协议框的箭头。
IPv4数据报发送到某个IPv6套接字的情形中,由IPv6套接字端的内核把收到的IPv4地址转换成IPv4映射的IPv6地址,并通过accept或recvfrom函数把映射后的IPv6地址透明地返回给应用进程;而通过某个IPv6套接字发送IPv4数据报的情形中,从IPv4地址到IPv4映射的IPv6地址之间的转换由解析器(如getaddrinfo函数)完成,映射后的IPv6地址随后由应用进程透明地传给connect或sendto函数。
上图中填有IPv4或IPv6的栏目表示相应的组合有效,并指出了实际使用的协议,标有(无)
的栏目表示相应的组合无效。最后一行第三列标了星号,因为该栏目的互操作性取决于客户选择的地址,如果选择AAAA记录从而发送IPv6数据报,就不能工作;如果选择A记录,这个A记录实际作为IPv4映射的IPv6地址返回给客户,使得客户发送的是IPv4数据报,那就能工作。
尽管以上表格中有四分之一的组合不能互操作,但在可预见的将来,IPv6的多数实现都运行在双栈主机上,而不是IPv6单栈实现,因此,我们如果删去上表中第二行和第二列,则所有不能互操作的栏目都消失了,只剩下标了星号的栏目。
有些IPv6应用必须要清楚与其通信的对端是不是IPv4对端,头文件netinet/in.h中定义了以下12个宏用于测试一个IPv6地址的类型:
前7个宏测试IPv6地址的基本类型,后5个宏测试IPv6多播地址的范围。
IPv4兼容的IPv6地址用于不被看好的某个过渡机制,不太可能实际看到这类地址,没有测试它的必要(IN6_IS_ADDR_V4COMPAT)。
IPv6客户可用IN6_IS_ADDR_V4MAPPED宏测试由解析器返回的IPv6地址,IPv6服务器可用该宏测试由accept或recvfrom函数返回的IPv6地址。
FTP的PORT指令是需要使用IN6_IS_ADDR_V4MAPPED宏的例子,如果启动一个FTP客户,登录到一个FTP服务器,然后发出FTP的dir命令,则FTP客户将通过控制连接向FTP服务器发送一个PORT指令,这条指令把客户的IP地址和端口号告知服务器,服务器随后据此建立一个数据连接。但IPv6的FTP客户必须清楚对端是IPv4服务器还是IPv6服务器,因为两者所需的PORT指令格式不同,如果对端是IPv4服务器,PORT的格式类似PORT a1,a2,a3,a4,P1,P2
,前4个数字(每个都在0~255之间)构成一个4字节IPv4地址,后2个数字构成2字节的端口号;如果对端是IPv6服务器,则需要一个EPRT指令,其中包含一个地址族、文本格式的地址、文本格式的端口号。
大多现有的网络应用是为IPv4编写的,这些应用填写一个或多个sockaddr_in结构,并且调用socket时第一个参数总是AF_INET。大多IPv4应用转换成IPv6应用并不费劲,有许多修改操作可用编辑脚本自动执行,较为依赖IPv4的程序转换起来比较麻烦,因为它们使用了诸如多播、IP首部选项字段、原始套接字等特性。
如果在源码上把一个应用转换成用IPv6的并发布它,我们还需考虑使用者的系统是否支持IPv6。这个考虑的典型处理方法是在代码中到处使用#ifdef伪代码,以尽可能使用IPv6(IPv6客户仍能与IPv4服务器通信,反之亦然)。这种方法在代码中插入许多#ifdef伪代码,在代码理解和维护上造成困难。
更好的方法是把程序向IPv6的转换视为促成程序变得协议无关的一个机会。首先去除所有gethostbyname和gethostbyaddr函数,改用getaddrinfo和getnameinfo函数,这使得我们能把套接字地址结构作为不透明对象来处理,就像bind、connect、recvfrom函数所做的那样,用一个指针及大小来引用它们。第三章中的sock_XXX函数中含有#ifdef伪代码以处理IPv4和IPv6的不同,这样将所有协议相关的内容隐藏在函数中将简化我们的代码。
另一点需要考虑的是,如果我们在一个同时支持IPv4和IPv6的系统上编译源代码,然后发布其可执行代码或目标文件(而不是源码),但某个使用者却在不支持IPv6的系统上执行我们的程序,会发生什么?有一种可能:本地名字服务器支持AAAA记录,能够为应用尝试连接的对端主机返回AAAA记录和A记录,当应用调用socket创建IPv6套接字时,如果本地主机不支持IPv6,则socket函数将失败。我们可以忽略socket函数的错误,继续尝试由名字服务器返回的地址列表中的下一个地址,这些细节可由第十一章中的getaddrinfo函数的接口函数来处理。假如对端主机有一个A记录,名字服务器在返回所有的AAAA记录后还返回了这个A记录,就有可能成功创建IPv4套接字。这类功能应由某个库函数提供,而不应出现在每个应用程序的源码中。
IPV6_ADDRFROM套接字选项可使服务器程序只收到IPv4或IPv6地址的套接字描述符,它可以返回特定地址族的套接字或潜在地修改套接字关联的地址的地址族,但其语义从未被完整描述,且它仅在特定情况下才有用,因此后来把它删除了。
双栈主机上的IPv6服务器既能服务IPv4客户,又能服务IPv6客户。IPv4客户发送给这种服务器的仍然是IPv4数据报,但服务器的协议栈会把客户主机的地址转换成IPv4映射的IPv6地址,因为IPv6服务器仅处理IPv6套接字地址结构。
双栈主机上的IPv6客户能和IPv4服务器通信,客户的解析器会把服务器主机所有的A记录作为IPv4映射的IPv6地址返回给客户,而客户指定这些地址之一调用connect将会使双栈发送一个IPv4 SYN分节。只有少量特殊客户和服务器需要知道对端使用的协议(如FTP),可在使用IPv6的程序中使用IN6_IS_ADDR_V4MAPPED宏判定对端是否在使用IPv4。
在一个运行IPv4和IPv6的双栈主机上启动一个IPv6的FTP客户,连接到一个IPv4的FTP服务器,确保客户处于主动模式(可能需要发出passive命令使服务器进入被动模式),发出debug命令,然后是dir命令。然后对一个IPv6的FTP服务器执行同样操作,比较由dir命令引发的两个PORT指令。以下是相关的摘录片段,省掉了登录和列目录等内容,主机freebsd上的FTP客户不论服务器使用IPv4还是IPv6,总是先尝试EPRT命令,若不工作则退回到PORT命令:
passive命令的作用是要进行数据传送从而要建立数据连接时,让服务器给出一个端口,然后客户主动连接到服务器的端口,passive是指服务器进入被动模式,FTP的主动模式指的是服务器进入主动模式,此时如果要建立数据连接,服务器会使数据连接的发起方。
如果FTP通信的双方的端口都是开放的,那么使用主动模式和被动模式都可以通信。而如果FTP的客户有防火墙,在FTP的主动模式下,FTP服务器会尝试连接FTP客户端的数据端口,但是由于客户端可能会有防火墙等网络安全设备的限制,导致连接失败。使用被动模式可以避免这个问题,因为在被动模式下,FTP服务器会监听一个端口(在passive命令的应答中返回),等待客户端连接然后发送给客户端数据。