代码之家  ›  专栏  ›  技术社区  ›  user2138149

双栈网络服务器无法按预期处理ipv4请求

  •  1
  • user2138149  · 技术社区  · 4 月前

    下面是一个实现双栈网络基础设施的C++服务器的最小工作示例。(这意味着单个套接字同时处理ipv4和ipv6连接。)

    #include <format>
    #include <print>
    #include <cstring>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netdb.h>
    #include <signal.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    
    int main(int argc, char* argv[]) {
    
        const auto SERVER_PORT = 7778;
    
        const auto server_fd = socket(AF_INET6, SOCK_STREAM, 0);
        int opt = 0;
    
        setsockopt(server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
    
        sockaddr_in6 server_address;
        std::memset(&server_address, 0, sizeof(server_address));
    
        server_address.sin6_family = AF_INET6;
        server_address.sin6_addr = in6addr_any;
        server_address.sin6_port = htons(SERVER_PORT);
        bind(server_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
    
        listen(server_fd, 10);
    
        sockaddr_storage peer_address;
        socklen_t peer_address_length = sizeof(peer_address);
        auto peer_fd = accept(server_fd, reinterpret_cast<sockaddr*>(&peer_address), &peer_address_length);
    
        if(peer_address.ss_family == AF_INET)
        {
            const auto p_peer_address = &peer_address;
            sockaddr_in* ipv4 = (sockaddr_in*)p_peer_address;
            std::println("Client port (IPv4): {}", ntohs(ipv4->sin_port));
        }
        else if(peer_address.ss_family == AF_INET6)
        {
            const auto p_peer_address = &peer_address;
            sockaddr_in6* ipv6 = (sockaddr_in6*)p_peer_address;
            std::println("Client port (IPv6): {}", ntohs(ipv6->sin6_port));
        }
        else
        {
            throw std::runtime_error("unrecognized ss_family");
        }
    
        close(peer_fd);
        close(server_fd);
    
        return 0;
    }
    

    然而,它并没有像预期的那样工作。无论客户端是通过ipv4还是ipv6连接,逻辑始终遵循 else if 分支 if 声明。第一个 如果 分支从不运行,表示并没有启动任何连接 AF_INET 家庭。

    实际上,这两个客户端似乎都正常工作。当连接启动时,服务器会做出响应——无论客户端连接类型如何,只会打印ipv6消息。(ipv4或ipv6)

    如果有区别,客户端和服务器都在同一台机器上运行,通过localhost连接( 127.0.0.1 )

    下面提供了这两种客户端的示例C++代码。

    // Client: ipv6
    
    #include <format>
    #include <print>
    #include <format>
    #include <cstring>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    
    
    int main(int argc, char* argv[]) {
    
        const auto PORT = 7778;
    
        const auto socket_fd = socket(AF_INET6, SOCK_STREAM, 0);
    
        in6_addr server_sin6_address;
        std::memset(&server_sin6_address, 0, sizeof(server_sin6_address));
        inet_pton(AF_INET6, "::1", &server_sin6_address);
    
        sockaddr_storage server_address;
        std::memset(&server_address, 0, sizeof(server_address));
    
        server_address.ss_family = AF_INET6;
        sockaddr_storage *p_server_address = &server_address;
        sockaddr_in6 *p_server_address_in6 = reinterpret_cast<sockaddr_in6*>(p_server_address);
        p_server_address_in6->sin6_family = AF_INET6; // why repeat?
        p_server_address_in6->sin6_port = htons(PORT);
        p_server_address_in6->sin6_flowinfo = 0; // not used?
        p_server_address_in6->sin6_addr = server_sin6_address;
        p_server_address_in6->sin6_scope_id = 0; // not used?
    
        const auto connect_result = connect(socket_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
    
        const char* const buffer = "hello world ipv6";
        const auto buffer_length = strlen(buffer) + 1;
        send(socket_fd, buffer, buffer_length, 0);
    
        close(socket_fd);
    
        return 0;
    }
    
    // Client: ipv4
    
    #include <format>
    #include <print>
    #include <format>
    #include <cstring>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    
    
    int main(int argc, char* argv[]) {
    
        const auto PORT = 7778;
    
        const auto socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    
        in_addr server_sin_address;
        std::memset(&server_sin_address, 0, sizeof(server_sin_address));
        inet_pton(AF_INET, "127.0.0.1", &server_sin_address);
    
        sockaddr_storage server_address;
        std::memset(&server_address, 0, sizeof(server_address));
    
        server_address.ss_family = AF_INET;
        sockaddr_storage *p_server_address = &server_address;
        sockaddr_in *p_server_address_in = reinterpret_cast<sockaddr_in*>(p_server_address);
        p_server_address_in->sin_family = AF_INET; // why repeat?
        p_server_address_in->sin_port = htons(PORT);
        p_server_address_in->sin_addr = server_sin_address;
        //p_server_address_in->sin_zero = 0; // not used?
    
        const auto connect_result = connect(socket_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
    
        const char* const buffer = "hello world ipv4";
        const auto buffer_length = strlen(buffer) + 1;
        const auto send_result = send(socket_fd, buffer, buffer_length, 0);
    
        close(socket_fd);
        std::println("Server quit");
    
        return 0;
    }
    

    也许这是ipv4客户端实现中的一个错误,但到目前为止,我没有发现任何看起来明显不正确的东西。

    到ipv6目标的ipv4连接是否会被操作系统或其他东西自动升级?这看起来真的很奇怪。

    1 回复  |  直到 4 月前
        1
  •  3
  •   Remy Lebeau    4 月前

    双栈套接字是一种IPv6套接字,可以与IPv4对等端通信(通过禁用 IPV6_V6ONLY 选项)。尽管如此,它仍然是一个IPv6套接字。

    AF_INET 套接字只能与 AF_INET 地址。同样,A AF_INET6 套接字只能与 AF_INET6 地址。因此,当客户被接受时 AF_INET6 服务器,无论服务器是否为双栈,接受的套接字也将是 AF_INET6 。这就是你所看到的。

    但是,对于双栈服务器,如果客户端使用IPv4而不是IPv6,则客户端的IP地址由 accept() getpeername() 将是一个 IPv4-mapped IPv6 address (a) AF_INET6 具有96位前缀的地址 0:0:0:0:0:FFFF 剩余的32位将是IPv4地址。

    如果你记录实际的IP地址,而不仅仅是端口,你就可以看到这一点。

    例如:

    if(peer_address.ss_family == AF_INET) // <-- never true for an AF_INET6 server!
    {
        const sockaddr_in* ipv4 = reinterpret_cast<sockaddr_in*>(&peer_address);
        char ipstr[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &ipv4->sin_addr, ipstr, sizeof(ipstr));
        std::println("Client IPv4: {}, port: {}", ipstr, ntohs(ipv4->sin_port));
    }
    else if(peer_address.ss_family == AF_INET6) // <-- always true for an AF_INET6 server!
    {
        const sockaddr_in6* ipv6 = reinterpret_cast<sockaddr_in6*>(&peer_address);
        char ipstr[INET6_ADDRSTRLEN];
        if (IN6_IS_ADDR_V4MAPPED(&sockaddr_in6->sin6_addr))
        {
            struct in_addr ipv4;
            memcpy(&ipv4.s_addr, &sockaddr_in6->sin6_addr.s6_addr[12], 4);
            inet_ntop(AF_INET, &ipv4, ipstr, sizeof(ipstr));
            std::println("Client IPv4 (mapped): {}, port: {}", ipstr, ntohs(ipv6->sin6_port));
        }
        else
        {
            inet_ntop(AF_INET6, &ipv6->sin6_addr, ipstr, sizeof(ipstr));
            std::println("Client IPv6: {}, port: {}", ipstr, ntohs(ipv6->sin6_port));
        }
    }
    else // <-- never happens on an AF_INET6 server!
    {
        throw std::runtime_error("unrecognized ss_family");
    }