克服 IPv6 向后兼容性方面的平台差异

问题描述

下面的代码是我在 C++ 中尝试使用最小的跨平台回显服务器。 它符合我的预期,除了平台相关的不对称方式 处理 IP 向后兼容性:在 Windows 上,与我尝试过的其他两个平台不同,IPv6 服务器无法处理来自 IPv4 客户端的请求。*

对我来说,其他平台可以做到这一点(下面的结果 #3)是一个意外但非常受欢迎的胜利。它为我开辟了一些可能性,前提是我也可以让它在 Windows 上工作。所以我的问题是:是否预期同样的事情在 Windows 上失败(比较结果 #3 和 #5)?我可以用服务器代码做些什么来让 #5 成功?

分解如下:

服务器使用 IPv4 (minimal_echo_server 8081 4):

  1. 客户端使用 IPv4 地址:服务器按预期响应
  2. 客户端使用 IPv6 地址:连接失败**如预期

服务器在 Ubuntu 20.04 或 macOS 10.13 (./minimal_echo_server 8081 6) 上使用 IPv6:

  1. 客户端使用 IPv4 地址:服务器响应良好(将客户端的 IP 呈现为 ::ffff: 后跟 IPv4 地址)
  2. 客户端使用 IPv6 地址:服务器按预期响应

服务器在 Windows 10 (.\minimal_echo_server.exe 8081 6) 上使用 IPv6:

  1. 客户端使用 IPv4 地址:连接失败**
  2. 客户端使用 IPv6 地址:服务器按预期响应

* 我所说的“IPv4 客户端”的一个例子可能是以下 netcat 调用

nc WWW.XXX.YYY.ZZZ 8081 # target the server via its IPv4 address

与以下客户端使用 IPv6 的情况相反:

nc fe80::WWWW:XXXX:YYYY:ZZZZ%en0 8081  # target the server via its IPv6 link-local address (with the client's local adapter name appended as scope)

** 在两种“失败”的情况下,失败模式似乎取决于客户端操作系统:Windows 10 客户端会暂停几秒钟,然后报告“无法建立连接因为目标机器主动拒绝了它”,而据我所知,我的 Linux 和 Darwin 客户端无限期地挂起。

// Welcome to  minimal_echo_server.cpp
// This can be compiled with `cl.exe minimal_echo_server.cpp` on Windows 10 using Visual Studio 2019
// or with `g++ -o minimal_echo_server minimal_echo_server.cpp` on something more GNUey.
#ifdef _WIN32
#   pragma comment(lib,"Ws2_32.lib")   // winsock2
#   include <winsock2.h>
#   include <ws2tcpip.h>   // for inet_pton() and inet_ntop()
#   define SOCKET_IS_VALID(S)      (S != INVALID_SOCKET)
#   define CLOSE_SOCKET closesocket
    typedef int socklen_t;
    bool InitializeSockets(void)
    {
        static bool initialized = false;
        WSADATA wsa;
        if(!initialized && WSAStartup(MAKEWORD(2,2),&wsa) == 0) initialized = true;
        return initialized;
    }
#else
#   include <sys/socket.h> // for socket(),bind(),etc
#   include <arpa/inet.h>  // for sockaddr_in and inet_ntop()
#   include <unistd.h>     // close()
#   define SOCKET_IS_VALID(S)     (S >= 0)
#   define CLOSE_SOCKET close
    typedef int SOCKET;
    bool InitializeSockets(void) { return true; }
#endif

#include <string.h> 
#include <string>   
#include <sstream>  
#include <iostream> 

#define USAGE "Mandatory first argument: port number (decimal integer > 0)\n" \
              "Optional second argument: 4 or 6 to denote IP version (default: 4)\n"
    
int main(int argc,const char * argv[])
{
    const int maxPendingConnections = 5;
    
    struct ::sockaddr_in  serverAddress4,remoteAddress4;
    struct ::sockaddr_in6 serverAddress6,remoteAddress6;
    struct ::sockaddr * addressptr;
    ::socklen_t addressSize;
    int domain;
    
    int port      = (argc > 1) ? ::atoi(argv[1]) : 0;
    int ipVersion = (argc > 2) ? ::atoi(argv[2]) : 4;   
    if(!port) { std::cerr << USAGE; return -1; }
    
    if(!InitializeSockets()) return -2;
    
    if(ipVersion == 4)
    {
        domain = PF_INET;
        addressptr = (struct ::sockaddr *)&serverAddress4;
        addressSize = (::socklen_t)sizeof(serverAddress4);
        ::memset(addressptr,addressSize);
        
        serverAddress4.sin_family = AF_INET;
        serverAddress4.sin_port = htons(port);
        serverAddress4.sin_addr.s_addr = htonl(INADDR_ANY);
    }
    else if(ipVersion == 6)
    {
        domain = PF_INET6;
        addressptr = (struct ::sockaddr *)&serverAddress6;
        addressSize = (::socklen_t)sizeof(serverAddress6);
        ::memset(addressptr,addressSize);
        
        serverAddress6.sin6_family = AF_INET6;
        serverAddress6.sin6_port = htons(port);
        serverAddress6.sin6_addr = in6addr_any;
        serverAddress6.sin6_scope_id = 0; // right?
    }
    else { std::cerr << USAGE; return -1; }
    
    SOCKET localServerSocket = ::socket(domain,SOCK_STREAM,IPPROTO_TCP);
    if(!SOCKET_IS_VALID(localServerSocket)) return -3;
    // to keep the example minimal,I will not be explicitly closing sockets on error
    if(::bind(localServerSocket,addressptr,addressSize) != 0) return -4;
    if(::listen(localServerSocket,maxPendingConnections) != 0) return -5;
    std::cerr << "listening on port " << port << " using IPv" << ipVersion << std::endl;
    while(true)
    {
        if(ipVersion == 4)
        {
            addressptr = (struct ::sockaddr *)&remoteAddress4;
            addressSize = (::socklen_t)sizeof(remoteAddress4);
        }
        else if(ipVersion == 6)
        {
            addressptr = (struct ::sockaddr *)&remoteAddress6;
            addressSize = (::socklen_t)sizeof(remoteAddress6);
        }
        SOCKET remoteConnectionSocket = ::accept(localServerSocket,&addressSize); 
        if(!SOCKET_IS_VALID(remoteConnectionSocket)) return -6;
        
        char buffer[128];
        std::string remoteAddressstring;
        if(ipVersion == 4)
        {
            remoteAddressstring = ::inet_ntop(AF_INET,&remoteAddress4.sin_addr,buffer,(socklen_t)sizeof(buffer)) ? buffer : "???";
        }
        else if(ipVersion == 6)
        {
            remoteAddressstring = ::inet_ntop(AF_INET6,&remoteAddress6.sin6_addr,(socklen_t)sizeof(buffer)) ? buffer : "???";
            if( IN6_IS_ADDR_LINKLOCAL( &remoteAddress6.sin6_addr ) )
            {
                std::stringstream ss;
                ss << "%" << remoteAddress6.sin6_scope_id;
                remoteAddressstring += ss.str();
            }
        }
        std::cerr << "  accepted connection from " << remoteAddressstring << std::endl;
        
        while(true)
        {
            char incomingData[32];
            int bytesReceived = ::recv(remoteConnectionSocket,incomingData,sizeof(incomingData),0);
            if(bytesReceived < 0) return -7;
            if(bytesReceived == 0) break;
            std::cerr << "  received " << bytesReceived << " bytes from " << remoteAddressstring << std::endl;
            int bytesSent = ::send(remoteConnectionSocket,bytesReceived,0); // echo
            if(bytesSent != bytesReceived) return -8;
        }
        CLOSE_SOCKET(remoteConnectionSocket);
        std::cerr << "  closed connection from " << remoteAddressstring << std::endl;
    }
    return 0; // never reached,but let's suppress the compiler warning
}

解决方法

我相信您遇到的问题是在 Windows 下,IPV6_V6ONLY 套接字选项默认设置为启用。为了在 Windows 下获得双栈套接字(可以在 IPv6 和 IPv4 上工作),您需要为您创建的每个 IPv6 套接字手动禁用该选项:

int v6OnlyEnabled = 0;  // we want v6-only mode disabled,which is to say we want v6-to-v4 compatibility
if (setsockopt(s,IPPROTO_IPV6,IPV6_V6ONLY,&v6OnlyEnabled,sizeof(v6OnlyEnabled)) != 0) printf("setsockopt() failed!?\n");
,

在 Windows 上,为了在 IPv6 服务器上接受 IPv4 客户端,服务器需要使用 Dual-Stack socket

默认情况下,在 Windows Vista 及更高版本上创建的 IPv6 套接字仅通过 IPv6 协议运行。为了使 IPv6 套接字成为双栈套接字,setsockopt 函数在套接字绑定到 IP 地址之前,必须使用 IPV6_V6ONLY 套接字选项调用以将此值设置为零。当 IPV6_V6ONLY 套接字选项设置为零时,为 AF_INET6 地址族创建的套接字可用于向 IPv6 地址或 IPv4 映射地址发送和接收数据包。