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

Overcoming platform differences in IPv6 backward compatibility

本文关键字:平台 方面 兼容性 IPv6 克服      更新时间:2024-09-27

下面的代码是我在C++中尝试的最小跨平台echo服务器。它做了我所期望的,除了方式上依赖于平台的不对称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, 0, 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, 0, 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, addressPtr, &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, buffer, (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, incomingData, 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客户端,服务器需要使用双堆栈套接字:

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