• Uncategorized

About c : Create-dual-stack-socket-on-all-loopback-interfaces-on-Windows

Question Detail

I an trying to create a dual-stack socket on Windows 7, for listening on both 127.0.0.1 and ::1 interfaces. I do not want to listen on all interfaces (0.0.0.0), just the loopback ones.

For dual-stack sockets, I have found that I need to disable the IPV6_V6ONLY option. I created a sample app which does just that (see below). When the app is running, netstat -an gives me the following output:

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

When connecting with putty on ::1, everything works. However, when I try to connect to 127.0.0.1, I get “Connection refused”.

When creating a socket on the “::” IPv6 address with the V6ONLY option disabled, I am able to connect on both IPv4 and IPv6, as expected.

So, how do I create a socket that listens on both IPv4 and IPv6 loopback interfaces?

Sample app I used for testing, adapted from here:

#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;
}

Question Answer

You have to create two sockets for this case. Creating a separate socket for each protocol/address is the normal way of doing this. There is the ::/0 with V6ONLY=0 shortcut if you want to listen on any address on both IPv4 and IPv6, but for anything other than that you’ll have to create multiple sockets.

Technically you can bind a server socket with V6ONLY=0 to an IPv4 mapped address, but there is not really any point in doing so. IN6ADDR_ANY is the only useful case. It makes the socket listen on any IPv6 address, including IPv4 mapped addresses (which are really IPv4 addresses in disguise).

Once you bind a socket to an address that binding acts like a filter, and only packets destined for that address will be received on the socket. And ::1 is a different address than ::ffff:127.0.0.1, so a socket bound to one will not receive messages for the other. You seem to assume that there is some implicit mapping between IPv4 and IPv6, but there isn’t except for the IPv4 mapped addresses. Each address is unique.

Once you bind a socket it will only work for one address. For other addresses you’d need extra sockets, so in those cases you might as well just create a real IPv4 socket.

When creating a socket on the “::” IPv6 address with the V6ONLY option disabled, I am able to connect on both IPv4 and IPv6, as expected.

That is exactly what you should be doing, and how it is designed to work.

A dual-stack socket must be created as an AF_INET6 socket, which means you have to bind() it to an IPv6 address. But your code is not ensuring that. IPV6_V6ONLY only works with an AF_INET6 socket, not an AF_INET socket.

When you call getaddrinfo(), you are using AF_UNSPEC, which gives getaddrinfo() permission to return of list of either AF_INET or AF_INET6 address(es), or even both at the same time. You are not taking that into account, or the order in which getaddrinfo() reports them. you are creating a socket for whatever address is first in the list. There may be more than one address reported.

So, either:

  1. set hints.ai_family to AF_INET6 so getaddrinfo() can only report AF_INET6 addresses, and then you can disable IPV6_V6ONLY on any AF_INET6 socket you create.

  2. set hints.ai_family to AF_UNSPEC, then create a separate socket for every address that getaddrinfo() actually reports (which you should be doing anyway), so you can create both AF_INET and AF_INET6 sockets at the same time. In this case, you should not disable IPV6_V6ONLY on any AF_INET6 socket you create if a corresponding AF_INET address was also reported.


Update: so, apparently an IPv4 client can connect to 127.0.0.1 (aka INADDR_LOOPBACK) on an IPv6 dual-stack listening socket only if the listening socket is bound to :: (aka IN6ADDR_ANY), which will also listen on 0.0.0.0 (aka INADDR_ANY). Binding to ::1 (aka IN6ADDR_LOOPBACK) will listen on 0.0.0.0 (instead of 127.0.0.1), but an IPv4 client cannot actually connect to 127.0.0.1. So, if you want to bind an IPv6 listening socket to ::1 and still allow IPv4 clients to connect to 127.0.0.1, you will have to create a separate IPv4 listening socket bound to 127.0.0.1. So that goes back to #2 above.

When binding a dual-stack listening socket to ::1, one would think it would listen on 127.0.0.1 instead of 0.0.0.0. Seems like a bug to me.

You may also like...

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.