Создайте сокет с двойным стеком на всех петлевых интерфейсах в Windows.

Я пытаюсь создать двойной сокет стека в Windows 7 для прослушивания интерфейсов 127.0.0.1 и ::1. Я не хочу слушать на всех интерфейсах (0.0.0.0), только на петлевых.

Для сокетов с двойным стеком я обнаружил, что мне нужно отключить IPV6_V6ONLY. Я создал пример приложения, которое делает именно это (см. ниже). Когда приложение запущено, netstat -an дает мне следующий вывод:

TCP    0.0.0.0:27015          0.0.0.0:0              LISTENING
TCP    [::1]:27015            [::]:0                 LISTENING

При подключении с помощью putty on ::1 все работает. Однако, когда я пытаюсь подключиться к 127.0.0.1, я получаю сообщение «Отказано в подключении».

При создании сокета на IPv6-адресе "::" с отключенной опцией V6ONLY я могу подключиться как к IPv4, так и к IPv6, как и ожидалось.

Итак, как мне создать сокет, который прослушивает петлевые интерфейсы IPv4 и IPv6?

Пример приложения, который я использовал для тестирования, адаптирован из здесь:

#include "stdafx.h"

#undef UNICODE

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// Need to link with Ws2_32.lib
#pragma comment (lib, "Ws2_32.lib")
// #pragma comment (lib, "Mswsock.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(void) 
{
    WSADATA wsaData;
    int iResult;

    SOCKET ListenSocket = INVALID_SOCKET;
    SOCKET ClientSocket = INVALID_SOCKET;

    struct addrinfo *result = NULL;
    struct addrinfo hints;

    int iSendResult;
    char recvbuf[DEFAULT_BUFLEN];
    int recvbuflen = DEFAULT_BUFLEN;

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    //hints.ai_flags = AI_PASSIVE;

    // Resolve the server address and port
    iResult = getaddrinfo("localhost", DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Create a SOCKET for connecting to server
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }

    // Disable V6ONLY, so we get IPv4 
    int disable = 0;
    iResult = setsockopt(ListenSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&disable, sizeof(disable));
    if (iResult == SOCKET_ERROR) {
        printf("setsockopt failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }

    // Setup the TCP listening socket
    iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    freeaddrinfo(result);

    iResult = listen(ListenSocket, SOMAXCONN);
    if (iResult == SOCKET_ERROR) {
        printf("listen failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // Accept a client socket
    ClientSocket = accept(ListenSocket, NULL, NULL);
    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // No longer need server socket
    closesocket(ListenSocket);

    // Receive until the peer shuts down the connection
    do {

        iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0) {
            printf("Bytes received: %d\n", iResult);

        // Echo the buffer back to the sender
            iSendResult = send( ClientSocket, recvbuf, iResult, 0 );
            if (iSendResult == SOCKET_ERROR) {
                printf("send failed with error: %d\n", WSAGetLastError());
                closesocket(ClientSocket);
                WSACleanup();
                return 1;
            }
            printf("Bytes sent: %d\n", iSendResult);
        }
        else if (iResult == 0)
            printf("Connection closing...\n");
        else  {
            printf("recv failed with error: %d\n", WSAGetLastError());
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }

    } while (iResult > 0);

    // shutdown the connection since we're done
    iResult = shutdown(ClientSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }

    // cleanup
    closesocket(ClientSocket);
    WSACleanup();

    return 0;
}

person cvlad    schedule 09.06.2016    source источник


Ответы (2)


Вы должны создать два сокета для этого случая. Создание отдельного сокета для каждого протокола/адреса является обычным способом сделать это. Существует ярлык ::/0 с V6ONLY=0, если вы хотите прослушивать любой адрес как в IPv4, так и в IPv6, но для чего-либо другого вам придется создать несколько сокетов.

Технически вы можете привязать сокет сервера с V6ONLY=0 к сопоставленному адресу IPv4, но на самом деле в этом нет никакого смысла. IN6ADDR_ANY - единственный полезный случай. Это заставляет сокет прослушивать любой адрес IPv6, включая сопоставленные адреса IPv4 (которые на самом деле являются замаскированными адресами IPv4).

Как только вы привязываете сокет к адресу, эта привязка действует как фильтр, и в сокет будут приниматься только пакеты, предназначенные для этого адреса. А адрес ::1 отличается от адреса ::ffff:127.0.0.1, поэтому сокет, привязанный к одному, не будет получать сообщения для другого. Кажется, вы предполагаете, что между IPv4 и IPv6 существует некое неявное сопоставление, но его нет, за исключением сопоставленных адресов IPv4. Каждый адрес уникален.

Как только вы привяжете сокет, он будет работать только для одного адреса. Для других адресов вам понадобятся дополнительные сокеты, поэтому в этих случаях вы можете просто создать настоящий сокет IPv4.

person Sander Steffann    schedule 09.06.2016
comment
Весь смысл создания сокета с двойным стеком заключается в том, что вам НЕ нужно создавать отдельные сокеты на одном и том же порту. - person Remy Lebeau; 09.06.2016
comment
Сокеты с двойным стеком являются исключением, их можно использовать в одном случае. Одностековые сокеты — это нормально и то, что вам нужно во всех остальных случаях. Для клиентского кода вы можете использовать метод 2 из ответа Реми. Для кода сервера вам нужно прослушивать несколько сокетов и использовать, например. select для обработки событий на этих нескольких сокетах. - person Sander Steffann; 10.06.2016
comment
Вопрос ОП относится к этому одному варианту использования в коде сервера. Сервер может связать сокет с двойным стеком как с IPv4, так и с IPv6 на одном порту. Клиент должен использовать AF_UNSPEC с getaddrinfo(), а затем перебирать весь список до тех пор, пока connect() успешно не подключится к одному из адресов, поскольку он не знает, по какому протоколу/адресу на самом деле может быть доступен сервер. - person Remy Lebeau; 10.06.2016
comment
Сервер может прослушивать сокет с двойным стеком только при прослушивании неопределенного адреса IPv6 (то есть всех адресов IPv4 и IPv6). ОП спрашивает, как использовать сокет с двойным стеком при прослушивании определенных адресов, и ответ: вы не можете. Возможно, это не то, что они хотят услышать, но это ответ... - person Sander Steffann; 10.06.2016
comment
Я смог воспроизвести проблему ОП. Я создал сокет IPv6 с двойным стеком, привязанный к ::1 (IN6ADDR_LOOPBACK), и я вижу, что он прослушивает как 0.0.0.0 (INADDR_ANY), так и ::1, но putty может подключаться только к ::1, а не к 127.0.0.1. Когда вместо этого я привязываюсь к :: (IN6ADDR_ANY), я вижу, что он прослушивает как 0.0.0.0, так и ::, но putty по-прежнему может подключаться только к ::1, а не 127.0.0.1. Когда я привязываю его к реальному IPv6-адресу адаптера, я вижу, что он прослушивает только этот IP-адрес, и шпатлевка может подключаться к этому IP-адресу, но не к IPv4-адресу того же адаптера. - person Remy Lebeau; 10.06.2016
comment
Неважно, я смог заставить putty подключаться к 127.0.0.1, когда сервер привязан к ::, но не к ::1. - person Remy Lebeau; 10.06.2016
comment
Правильный. Так и задумано: V6ONLY=0 работает только с IN6ADDR_ANY - person Sander Steffann; 10.06.2016
comment
@Steffann Я не смог найти никакой документации, намекающей на то, что это дизайн, а не ошибка. У вас есть такие ссылки? - person cvlad; 10.06.2016
comment
Я расширю свой ответ, чтобы прояснить ситуацию - person Sander Steffann; 10.06.2016
comment
@SanderSteffann Спасибо, теперь это совершенно ясно и очевидно: IN6ADDR_ANY заставляет сокет прослушивать любой адрес IPv6, включая сопоставленные адреса IPv4. - person Pavel P; 16.02.2018

При создании сокета на IPv6-адресе "::" с отключенной опцией V6ONLY я могу подключиться как к IPv4, так и к IPv6, как и ожидалось.

Это именно то, что вы должны делать, и как это должно работать.

Сокет с двойным стеком должен быть создан как сокет AF_INET6, что означает, что вы должны bind() связать его с адресом IPv6. Но ваш код не гарантирует этого. IPV6_V6ONLY работает только с сокетом AF_INET6, а не с сокетом AF_INET.

Когда вы вызываете getaddrinfo(), вы используете AF_UNSPEC, что дает getaddrinfo() разрешение на возврат списка либо AF_INET или AF_INET6 адресов, либо даже обоих одновременно. Вы не принимаете это во внимание или порядок, в котором getaddrinfo() сообщает о них. вы создаете сокет для любого адреса, который находится первым в списке. Может быть указано более одного адреса.

Итак, либо:

  1. установите hints.ai_family на AF_INET6, чтобы getaddrinfo() мог сообщать только AF_INET6 адреса, а затем вы можете отключить IPV6_V6ONLY на любом созданном вами сокете AF_INET6.

  2. установите hints.ai_family на AF_UNSPEC, затем создайте отдельный сокет для каждого адреса, который фактически сообщает getaddrinfo() (что вы должны сделать в любом случае), чтобы вы могли создавать сокеты AF_INET и AF_INET6 одновременно. В этом случае вам не следует отключать IPV6_V6ONLY для любого созданного вами сокета AF_INET6, если также был сообщен соответствующий адрес AF_INET.


Обновление: очевидно, что клиент IPv4 может подключаться к 127.0.0.1 (также известному как INADDR_LOOPBACK) через прослушивающий сокет с двойным стеком IPv6, только если прослушивающий сокет привязан к :: (также известному как IN6ADDR_ANY), который также будет прослушивать 0.0.0.0 (он же INADDR_ANY). Привязка к ::1 (он же IN6ADDR_LOOPBACK) будет прослушивать 0.0.0.0 (вместо 127.0.0.1), но клиент IPv4 фактически не может подключиться к 127.0.0.1. Итак, если вы хотите привязать прослушивающий сокет IPv6 к ::1 и по-прежнему разрешать клиентам IPv4 подключаться к 127.0.0.1, вам придется создать отдельный прослушивающий сокет IPv4, привязанный к 127.0.0.1. Итак, возвращаемся к пункту 2 выше.

При привязке прослушивающего сокета с двойным стеком к ::1 можно было бы подумать, что он будет прослушивать 127.0.0.1 вместо 0.0.0.0. Мне кажется ошибка.

person Remy Lebeau    schedule 09.06.2016