IPv4向IPv6迁移有多种途径,在选择具体的迁移方式时,当前环境中运行的应用是否支持IPv6是重要的考量因素之一,同时在编写新的应用时,需要考虑新编写的应用不仅可以适应当前主流的IPv4环境,还要能运行于IPv6环境c socket。当前主要的ICT基础设施都支持IPv6,但目前仍有众多的客户应用是基于IPv4环境开发的,如果在不修改应用以支持IPv6访问的情形下,所有的迁移方式都难言是彻底的。这篇短文拟对IPv4应用向IPv6应用迁移涉及到的相关内容进行简要介绍,双栈改造的方式有多种,本文以ipv4- mapped ipv6为例,期待对需要了解IPv6应用迁移的读者有所帮助。
2. IPv4-mapped IPv6地址
IPv4-mapped IPv6地址格式是::FFFF:IPv4-address,例如::FFFF:192.168.1.1,运行在双协议栈设备上的IPv6应用利用IPv4-mapped IPv6地址可以和基于IPv4的应用互通c socket。
基于IPv6的服务端应用在接收到来自IPv4客户端的连接请求时,内部采用IPv4-mapped IPv6地址来表示此连接c socket。
基于IPv6的客户端应用连接IPv4的服务端时c socket,采用IPv4-mapped IPv6的地址访问对端.
IPv4-mapped IPv6地址只用在系统内部,不能设置在网卡上作为IPv6的地址,也不会出现在网络数据包的源或目的IP地址字段中c socket。
3. IP双栈环境下应用访问
IP双栈主机同时运行IPv4和IPv6协议c socket,配置IPv4和IPv6地址,可以同时和IPv4以及IPv6设备进行通讯:
图1 IPv4以及IPv6设备同时进行通讯
双栈环境下的不同的网络客户端应用发送数据包的协议路径:
展开全文
图2 不同的网络客户端应用发送数据包的协议路径
双栈环境下不同的网络服务端应用接受数据包的协议路径:
图3 不同的网络服务端应用接受数据包的协议路径
4. IPv4客户访问 IPv4服务
这是当前最常见的访问方式,IPv4的客户端在创建socket时采用的IPv4地址族AF_INET,IPv4服务端的socket也采用IPv4地址族AF_INET,客户端和服务端的网络通信都通过IPv4协议栈,客户端和服务端的通讯采用的源和目的IP是IPv4地址c socket。
图4 IPv4客户访问IPv4服务
5. IPv4客户访问 IPv6服务
留存的客户端网络应用在早期创建socket时采用的地址族为IPv4 地址族AF_INET,但服务端为了适应IPV6环境,经过改造,在创建socket时采用的是IPv6地址族AF_INET6,并且在bind调用时不指定具体的IPv6地址或指定IPv4-mapped IPv6地址(实际应用基本不会采用,技术上可行),那么IPv4客户端可以通过连接对端可通达的IPv4地址来访问服务c socket。
客户端主机上发送和接受的网络包通过IPv4协议栈,服务端主机从IPv4协议栈接受到数据包,根据IPv4数据包中的传输层协议发送给TCP或UDP模块处理,TCP/UDP模块查找TCP/UDP协议控制块,找到符合条件的的协议控制块(如目的端口相同以及V6only为0等)来进一步接收/处理数据包,同时在协议栈内部用IPv4-mapped IPv6地址来标记此连接c socket。
图5 IPv4客户访问IPv6服务
下文中的所有python的示例都是基于Centos 7.5c socket,采用的python版本是2.7,示例仅为功能演示,错误处理都已忽略:
创建一个 IPv6 地址族的 socket, 绑定服务监听端口 7788 c socket,等待客户的连接请求,服务端接收 IPv4/v6 客户连接请求并输出接收到的数据,并打印相关的 TCP 连接信息
#python
>>> from socket import *
>>> s=socket(AF_INET6,SOCK_STREAM)
>>> s.bind(("",7788))
>>> s.listen(5)
>>> s1,c1=s.accept
>>> s1.recv(100)
'aaaaaaaaa'
>>> print c1
('::ffff:192.168.100.165', 33178, 0, 0)
>>> from os import *
>>> system("ss -tn|grep 7788")
ESTAB 0 0 ::ffff:192.168.100.128:7788 ::ffff:192.168.100.165:33178
>>>
创建 IPv4 地址族的客户端 , 连接服务器 192.168.100.128 上的服务端口 7788 并发送数据c socket,同时在系统层面用 ss 命令输出 TCP 的连接信息
#python
>>> from socket import *
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.connect(("192.168.100.128",7788))
>>> s.send(b"aaaaaaaaa")
9
>>> from os import *
>>> system("ss -tn|grep 7788")
ESTAB 0 0 192.168.100.165:33178 192.168.100.128:7788
>>>
6. IPv6客户访问 IPv6服务
新开发的网络客户端和服务端应用socket建议采用IPv6地址族的AF_INET6,此类通讯是向IPv6迁移的最终目标,通讯的源和目的地址都是128bit的IPv6地址c socket。
缺省情形下IPv6的服务端可以同时接受来自IPv4和IPv6的客户端的连接请求,在只有IPv6的环境下,可以通过setsockopt设置IPv6_V6ONLY为1或设置sysctl的参数net.IPv6.bindv6Only=1(LINUX环境)使服务程序只接受来自IPv6地址族的连接请求c socket。在IPv6的客户端也可以采用同样设置,客户端只能和IPv6的地址进行通讯。
图6 IPv6客户访问IPv6服务
7. IPv6客户端访问 IPv4服务端
新开发的网络客户端为了适应IPv6环境,采用IPv6地址族AF_INET6的socket,但留存的IPv4的服务端暂时无法改造,仍然采用IPv4地址族AF_INET,由于客户端采用的是IPv6地址族,IPv6的客户端访问IPv4服务端可以利用服务端的IPv4-mapped IPv6 地址方式和IPv4的服务端通讯c socket。
IPv6客户端通过本机的IPv4协议栈,数据包头的源和目的网络层地址都是标准的IPv4地址,IPv4服务端无法区分客户端应用是基于IPv4地址族还是基于IPv6地址族c socket。
图7 IPv6客户端访问IPv4服务端
创建 IPv4 地址族的 socket c socket,绑定服务监听端口 8888 ,在此端口上接收客户连接请求并输出接收到的数据以及连接请求客户端的 IP 地址以及 TCP 端口号信息
#python
>>> from socket import *
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.bind(("",8888))
>>> s.listen(4)
>>> s1,c1=s.accept
>>> s1.recv(100)
'xxxxxxxxx'
>>> print c1
('192.168.100.128', 49314)
>>> from os import *
>>> system("ss -tn")
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.100.165:8888 192.168.100.128:49314
>>>
创建 IPv6 地址族的 socket 客户端 , 用 IPV4-mapped 地址连接服务端 192.168.100.165 上的服务监听端口 8888 c socket,连接成功后发送数据给服务端,同时在系统层面采用 ss 命令输出 TCP 的连接信息
#python
>>> from socket import *
>>> s=socket(AF_INET6,SOCK_STREAM)
>>> s.connect(("::ffff:192.168.100.165",8888))
>>> s.send("xxxxxxxxx")
>>> from os import *
>>> system("ss -tn")
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 ::ffff:192.168.100.128:49314 ::ffff:192.168.100.165:8888
8. IPv6 V6ONLY应用
基于IPv6地址族的应用缺省情形下可以和基于IPv4以及IPv6地址族的应用互通,设置IPv6_V6ONLY参数可以改变此行为,此参数在HPUX、Linux环境下缺省为0,Windows环境下缺省为1,如果设置为1,那么应用只能和IPv6地址族的应用互通,在一个只有IPv6的网络环境下,可以设置IPv6_V6ONLY为真c socket。下面针对客户端和服务端的应用设置分别说明.
IPv6 地址族的 socket 设置 V6ONLY 选项为真的客户端访问 IPv4 服务端 ( 失败 )
在IPv6的客户端程序中调用setsockopt来设置IPv6_V6ONLY为真,那么此客户端只能和IPv6的服务端进行通讯,不能通过IPv4-mapped IPv6地址的形式和IPv4的服务端进行通讯c socket。
图8 IPv6 V6ONLY应用
IPv4 客户访问 IPv6 V6ONLY 服务端 ( 失败 )
采用IPv6 地址族AF_INET6的socket 的服务端应用缺省能接受来自IPv4和IPv6客户端的连接请求(windows系统下IPv6地址族的应用V6ONLY缺省为真),在只有IPv6的环境下,调用setsockopt设置IPv6_V6ONLY为1来拒绝来自IPv4 的客户端连接请求,只接受来自IPv6 客户端的连接请求c socket。
由于服务端收到IPv4客户端的地址是IPv4的,服务端在查找到对应的TCP/UDP协议控制块后,发现协议控制块被标记为V6ONLY,故拒绝连接请求c socket。
图9 IPv6 V6ONLY应用
V6ONLY服务端示例c socket,创建一个基于IPv6地址族的socket,设置新建socket的IPV6_V6ONLY的选项为真,绑定服务端口9999并启动服务监听
# python
>>> from socket import *
>>> from os import *
>>> s=socket(AF_INET6,SOCK_STREAM)
>>> s.setsockopt(IPPROTO_IPV6,IPV6_V6ONLY,1)
>>> s.bind(('',9999))
>>> s.listen(8)
>>> system("ss -6ltne|grep 9999")
>>>
9. 现有应用改造
在具备源代码的情形下,将一个IPv4的服务端程序改造成支持IPv6的程序,改造后的应用能同时接受来自IPv4和IPv6客户端的服务请求,改造主要涉及到的是有关地址结构的变化c socket。
以C源码为例c socket,现有的支持IPv4 地址族的服务端代码:
int sock,sock_new;
unsigned short srv-port;
struct sockaddr_in serv,cli;
socklen_t cli_len=soizeof(cli);
sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
memset((&serv,0,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_len=sizeof(serv);
serv.sin_port=htons(srv-port));
bind(sock,(struct sockaddr*)&serv,sizeof(serv));
listen(sock,5);
sock_new=accept(sock,(struct sockaddr*)&cli,&len);
改造后的基于IPv6地址族的服务程序c socket,可以同时接受来自IPv4和IPv6 客户端的连接请求:
int sock,sock_new;
unsigned short srv-port;
struct sockaddr_in6 serv,cli;
socklen_t cli_len=soizeof(cli);
sock=socket (AF_INET6,SOCK_STREAM,IPPROTO_TCP);
memset((&serv,0,sizeof(serv));
serv.sin6_family=AF_INET6;
serv.sin6_len=sizeof(serv);
serv.sin6_port=htons(srv-port));
bind(sock,(struct sockaddr*)&serv,sizeof(serv));
listen(sock,5);
sock_new=accept(sock,(struct sockaddr*)&cli,&len);
10. IPv6地址族服务端程序设计
在协议迁移的过渡阶段c socket,为了能同时响应来自IPv4和IPv6的客户端应用请求,服务端程序的设计常见有两种方式:
只创建 1 个 IPv6 地址族的 socket 监听
只建立一个AF_INET6地址族的监听socket来同时响应来自IPv4和IPv6客户端的服务请求c socket。当接受到的请求目的地址属于IPv4地址族时,服务器内部采用IPv4-mapped IPv6地址的方式来表示,此连接请求通过IPv4协议栈处理,当接受到的请求目的地址属于IPv6地址族时,连接请求通过IPv6协议栈处理。
Linux上常用的ftp服务器vsftpd采用此类方式实现c socket,从下面的输出可以看出,针对TCP Port 21只有一条基于AF_INET6地址族的监听项,并且V6ONLY选项是0,也即意味着此TCP port 21可以同时接受来自IPv4和IPv6客户端的连接请求:
#ss -6ltnep|grep :21
LISTEN 0 32 :::21:::* users:(( "vsftpd",pid=1290,fd=3)) ino:16150 sk:12 v6only:0 <->
同时创建 1 个 IPv4 socket 监听和 1 个 IPv6 socket 监听
为了避免在内核中采用IPv4-mapped IPv6地址,在同一个服务端程序中分别创建AF_INET和AF_INET6两个地址族的socket,同时监听在相同的端口上,IPv4地址族AF_INET的监听socket负责接受来自IPv4客户端的连接,并且设置IPv6地址族socket的IPv6_V6ONLY属性为真,从而只负责接受来自IPv6客户端的连接,由于有两个处于监听状态的socket, 调用select来处理来自不同socket地址族的连接请求c socket。
Linux系统中sshd的是采用此方式实现的c socket,从strace跟踪sshd相关系统调用的输出或查看openssh的源码都可以验证:
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(22), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 128) = 0
socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP) = 4
setsockopt(4, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
setsockopt(4, SOL_IPv6, IPv6_V6ONLY, [1], 4) = 0
bind(4, {sa_family=AF_INET6, sin6_port=htons(22), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
listen(4, 128) = 0
select(5, [3 4], NULL, NULL, NULL)
ss指令输出可以直观的表明其机制:
# ss -ltnep|grep 22
LISTEN 0 128 *:22*:* users:(( "sshd",pid=3236,fd=3)) ino:88651 sk:1f <->
LISTEN 0 128 :::22 :::* users:(( "sshd",pid=3236,fd=4)) ino:88653 sk:20 v6only:1<->
11. IPv6客户端程序设计
IPv6客户端通过指定地址或域名来访问IPv4以及IPv6的服务端,如果服务提供方是IPv4,那么需要用IPv4-mapped IPv6地址的形式去访问c socket。针对域名访问,需要采用DNS实现名称来解析,在IPv4环境下的解析库函数调用有两个:gethostbyname(正向解析)、gethostbyaddr(反向解析),此类解析用于IPv4环境。IPv6环境下则采用getaddrinfo和getnameinfo来解析IPv4和IPv6地址信息,getaddrinfo根据域名/服务名称以及相关hints等信息返回一组A和AAAA记录,每条返回的记录附加有用于创建socket所需的AF_xxx以及SOCK_xxx等信息,客户端程序通常利用循环语句针对getaddrinfo返回地址列表的每一项尝试创建socket以及connect对端,直到发现针对一条地址记录的socket和connect调用都返回成功为止。
客户端只需要知道服务端的域名以及服务端口,采用getaddrinfo调用返回的地址列表信息,就可以创建合适的地址族的socket和服务端连接c socket。
Python 的 getaddrinfo 使用示例 :
解析地址的列表
#python
>>> from socket import *
>>>from os import *
>>> from pprint import pprint
>>> list=getaddrinfo("")
>>> pprint(list)
[(2, 1, 6, '', ('23.7.213.221', 80)),
(2, 2, 17, '', ('23.7.213.221', 80)),
(10, 1, 6, '', ('2600:1417:a000:195::1463', 80, 0, 0)),
(10, 2, 17, '', ('2600:1417:a000:195::1463', 80, 0, 0)),
(10, 1, 6, '', ('2600:1417:a000:1b4::1463', 80, 0, 0)),
(10, 2, 17, '', ('2600:1417:a000:1b4::1463', 80, 0, 0))]
>>> list[0][0:3]
(2, 1, 6)
>>> list[0][4]
('23.7.213.221', 80)
连接DNS解析返回列表中的第一项的IPv4地址
c socket,成功后在系统层面输出TCP连接的四元组信息:
>>> s=socket(*list[0][0:3])
>>> s.connect(list[0][4])
>>> system("ss -tn|grep 80")
ESTAB 0 0 192.168.100.165:39838 23.7.213.221:80 0
尝试用DNS解析返回列表中的第三项IPv6的地址信息来连接地址,连接失败:
>>> s1=socket(*list[2][0:3])
>>> s1.connect(list[2][4])
Traceback (most recent call last):
File "
File "/usr/lib64/python2.7/socket.py", line 224, in meth
return getattr(self._sock,name)(*args)
socket.error: [Errno 101] Network is unreachable
>>>
C 语言的 getaddrinfo 使用示例:
main(int argc, char **argv)
struct addrinfo *res, *ainfo;
struct addrinfo hints;
int error;
struct sockaddr_in6 peeraddr6;
struct sockaddr_in6 addr6;
char connect_addr[INET6_ADDRSTRLEN];
if (argc != 2) {
fprintf(stderr, "Usage: %s
exit(1);
memset ((char *)&hints, 0, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
error = getaddrinfo(argv[1], ");
if (error != 0)
exit(1);
for (ainfo = res; ainfo != NULL; ainfo = ainfo->ai_next) {
s = socket (ainfo->ai_family,ainfo->ai_socktype,ainfo->ai_protocol);
if (s == -1)
continue;
if (connect(s, ainfo->ai_addr, ainfo->ai_addrlen) == -1)
continue;
else
break;
java,Python等语言对这些细节都做了很好的封装,如python中的socket.create_connection就可以实现以上C代码的所有功能
c socket。
12. 结束语
本文对基于不同IP地址族的应用的访问方式以及具体实现的方法进行了简要说明,同时也介绍了如何改造现有IPv4应用以适应新的IPv6环境,介绍了同时适用于IPv4以及IPv6环境的应用的方法
。IPv6应用迁移是未来应用迁移发展的方向,希望本文内容对IPv6应用迁移感兴趣的读者有所帮助。