IPv6与IPv4的互操作性
缘起
最近看见很多app的启动页面上都支持IPv6标志。说起来IPv6已经出现很多年了,但是实际应用中却一直没有能被广泛使用。
而实际上早在2000年以前操作系统就已经能支持双协议栈(dual stacks),可以实现IPv4和v6的互通了。
实现原理
IPv4 和 IPv6 互联时,操作系统内核根据具体情况,在数据包通过IP协议层时将IPv6地址和IPv4地址互相转化,应用层不需要知道转化的细节
IPv4地址可以映射层IPv6地址,用户代码唯一要做的是调用getaddrinfo进行dns解析时,hints参数设置AI_V4MAPPED标志
下面时具体步骤
IPv4客户端连接IPv6服务端
- IPv6的服务器启动IPv6套接字
//note/c/network/server_compatible_ipv6.c listenfd = socket(AF_INET6, SOCK_STREAM, 0); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin6_family = AF_INET6; // ipv6的服务器默认地址是 /* :: */ // 回环地址时 /* ::1 */ serv_addr.sin6_addr = in6addr_any; /* :: */ serv_addr.sin6_port = htons(port); bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(listenfd, 10);
- IPv4调用gethostbyname找到服务器主机的A记录(服务器主机还有一个AAAA记录,对应ipv6地址)
//note/c/network/client_compatible_ipv6.c struct addrinfo hists, *res; bzero(&hists, sizeof(hists)); hists.ai_flags = AI_CANONNAME; hists.ai_family = AF_UNSPEC; // 此处没有使用使用gethostbyname,因为这个函数不支持ipv6,但ipv4的客户端一般都是使用gethostbyname // getaddrinfo 的 man 文件中显示这个函数支持的版本,需要定义编译常量-D_POSIX_C_SOURCE=200112L(或更大) // 但我实际编译的时候不定义也没问题,只是vscode会提示有错误 getaddrinfo(hostname, server, &hists, &res); do{ if(!res) { return; } sockfd = socket(res->ai_family, SOCK_STREAM, 0); if(res->ai_family == AF_INET) { if(connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) { break; } } // if(res->ai_family == AF_INET6) // { // if(connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) // { // break; // } // } close(sockfd); }while((res = res->ai_next) != NULL);
- 客户调用connect发送IPv4的SYN分节
- 服务主机收到这个IPv4 SYN分节,内核设置一个标志指示本连接使用IPv4映射的IPv6地址,同时响应客户一个 IPv4的 SYN/ACK 分节,而此时服务主机的用户代码中acceptf返回的时这个IPv4映射的IPv6地址
//note/c/network/server_compatible_ipv6.c connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &len); // IN6_IS_ADDR_V4MAPPED 宏可以测试ipv6地址是否时ipv4映射的 // 下面是这个宏的代码 // # define IN6_IS_ADDR_V4MAPPED(a) \ // (__extension__ \ // ({ const struct in6_addr *__a = (const struct in6_addr *) (a); \ // __a->__in6_u.__u6_addr32[0] == 0 \ // && __a->__in6_u.__u6_addr32[1] == 0 \ // && __a->__in6_u.__u6_addr32[2] == htonl (0xffff); })) if(IN6_IS_ADDR_V4MAPPED(&cli_addr.sin6_addr)) { printf("v4mapped address\n"); }
- 服务主机向这个IPv4映射的IPv6地址发送TCP分节时,IP栈的目的地址又被内核转换为IPv6地址映射的IPv4地址.
IPv6客户端连接IPv4服务端
- IPv4服务器启动一个IPv4监听套接字(只有A记录)
- IPv6客户启动后调用getaddrinfo获得服务器IPv6地址(hints结构中设置AI_V4MAPPED标志),因为IPv4服务器只有A记录,getaddrinfo返回的是IPv4映射的IPv6地址
//note/c/network/netaddr.c struct addrinfo hists, *res; char buf[255]; bzero(&hists, sizeof(hists)); // hists.ai_flags = AI_CANONNAME; hists.ai_flags = AI_V4MAPPED; // hists.ai_family = AF_UNSPEC; hists.ai_family = AF_INET6; getaddrinfo(argv[1], argv[2], &hists, &res);
观察一下ipv4和ipv6如何转换映射的
gcc netaddress.c -o netaddr ./netaddr qq.com 8888 call getaddrinfo start // 可以看到ipv4映射的ipv6地址的样子 ::ffff:111.161.64.48 qq.com
- IPv6客户调用 connect 连接服务主机时,内核检测到这个映射地址后发送一个IPv4 SYN分节到服务器
- 服务器响应 SYN/ACK 分节, 连接建立