2

This is a continuation of Connecting IPv4 client to IPv6 server: connection refused. I am experimenting with dual stack sockets and trying to understand what setsockopt with IPV6_V6ONLY is useful for. On the linked question I was advised that "Setting IPV6_V6ONLY to 0 can be useful if you also bind the server to an IPv6-mapped IPv4 address". I have done this below, and was expecting my server to be able to accept connections from both an IPv6 and an IPv4 client. But shockingly when I run my client with a V4 and a V6 socket, neither can connect!

Can someone please tell me what I am doing wrong, or have I misunderstood IPv6 dual stack functionality all together?

Server:

void ConvertToV4MappedAddressIfNeeded(PSOCKADDR pAddr)
{
// if v4 address, convert to v4 mapped v6 address
if (AF_INET == pAddr->sa_family)
{
    IN_ADDR In4addr;
    SCOPE_ID scope = INETADDR_SCOPE_ID(pAddr);
    USHORT port = INETADDR_PORT(pAddr);
    In4addr = *(IN_ADDR*)INETADDR_ADDRESS(pAddr);
    ZeroMemory(pAddr, sizeof(SOCKADDR_STORAGE));
    IN6ADDR_SETV4MAPPED(
        (PSOCKADDR_IN6)pAddr,
        &In4addr,
        scope,
        port
        );
    }
} 

addrinfo* result, hints;

memset(&hints, 0, sizeof hints); 
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;

int nRet = getaddrinfo("powerhouse", "82", &hints, &result);

SOCKET sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);

int no = 0;
if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&no, sizeof(no)) != 0)
    return -1;

ConvertToV4MappedAddressIfNeeded(result->ai_addr);

if (bind(sock, result->ai_addr, 28/*result->ai_addrlen*/) ==  SOCKET_ERROR)
    return -1;

if (listen(sock, SOMAXCONN) == SOCKET_ERROR)
    return -1;

SOCKET sockClient = accept(sock, NULL, NULL);
printf("Got one!\n");

Client:

addrinfo* result, *pCurrent, hints;
char szIPAddress[INET6_ADDRSTRLEN];

memset(&hints, 0, sizeof hints);    // Must do this!
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;

const char* pszPort = "82";

if (getaddrinfo("powerhouse", "82", &hints, &result) != 0)
    return -1;

SOCKET sock = socket(AF_INET, result->ai_socktype, result->ai_protocol);
int nRet = connect(sock, result->ai_addr, result->ai_addrlen);  
4

1 回答 1

9

My C skills are a bit rusty, so here is a counter-example written in Python. My local IPv4 address is 37.77.56.75, so that is what I will bind to. I kept it as simple as possible to focus on the concepts.

This is the server side:

#!/usr/bin/env python
import socket

# We bind to an IPv6 address, which contains an IPv6-mapped-IPv4-address,
# port 5000 and we leave the flowinfo (an ID that identifies a flow, not used
# a lot) and the scope-id (basically the interface, necessary if using
# link-local addresses)
host = '::ffff:37.77.56.75'
port = 5000
flowinfo = 0
scopeid = 0
sockaddr = (host, port, flowinfo, scopeid)

# Create an IPv6 socket, set IPV6_V6ONLY=0 and bind to the mapped address
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.bind(sockaddr)

# Listen and accept a connection
sock.listen(0)
conn = sock.accept()

# Print the remote address
print conn[1]

Here we bind to an IPv6 address in the code, but the address is actually an IPv6-mapped IPv4 address, so in reality we are binding to an IPv4 address. This can be seen when looking at i.e. netstat:

$ netstat -an | fgrep 5000
tcp4       0      0  37.77.56.75.5000       *.*                    LISTEN     

We can then use an IPv4 client to connect to this server:

#!/usr/bin/env python
import socket

# Connect to an IPv4 address on port 5000
host = '37.77.56.75'
port = 5000
sockaddr = (host, port)                   

# Create an IPv4 socket and connect
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 
conn = sock.connect(sockaddr)

And the server will show us who connected, using IPv6 address representation:

('::ffff:37.77.56.76', 50887, 0, 0)

In this example I connected from IPv4 host 37.77.56.76, and it choose port 50887 to connect from.

In this example we are only listening on an IPv4 address (using IPv6 sockets, but it is still an IPv4 address) so IPv6-only clients will not be able to connect. A client with both IPv4 and IPv6 could of course use IPv6 sockets with IPv6-mapped-IPv4-addresses, but then it would not really be using IPv6, just an IPv6 representation of an IPv4 connection.

A dual-stack server has to either:

  1. listen on the wildcard address, which will make the OS accept connections on any address (both IPv4 and IPv6)
  2. listen on both an IPv6 address and an IPv4 address (either by creating an IPv4 socket, or by creating an IPv6 socket and listening to an IPv6-mapped-IPv4-address as shown above)

Using the wildcard address is the most simple. Just use the server example from above and replace the hostname:

# We bind to the wildcard IPv6 address, which will make the OS listen on both
# IPv4 and IPv6
host = '::'
port = 5000
flowinfo = 0
scopeid = 0
sockaddr = (host, port, flowinfo, scopeid)

My Mac OS X box shows this as:

$ netstat -an | fgrep 5000
tcp46      0      0  *.5000                 *.*                    LISTEN     

Notice the tcp46 which indicates that it listens on both address families. Unfortunately on Linux it only shows tcp6, even when listening on both families.

Now for the most complicated example: listening on multiple sockets.

#!/usr/bin/env python
import select
import socket

# We bind to an IPv6 address, which contains an IPv6-mapped-IPv4-address
sockaddr1 = ('::ffff:37.77.56.75', 5001, 0, 0)

sock1 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
sock1.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock1.bind(sockaddr1)
sock1.listen(0)

# And we bind to a real IPv6 address
sockaddr2 = ('2a00:8640:1::224:36ff:feef:1d89', 5001, 0, 0)

sock2 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
sock2.bind(sockaddr2)
sock2.listen(0)

# Select sockets that become active
sockets = [sock1, sock2]
readable, writable, exceptional = select.select(sockets, [], sockets)
for sock in readable:
    # Accept the connection
    conn = sock.accept()

    # Print the remote address
    print conn[1]

When running this example both sockets are visible:

$ netstat -an | fgrep 5000
tcp6       0      0  2a00:8640:1::224.5000  *.*                    LISTEN     
tcp4       0      0  37.77.56.75.5000       *.*                    LISTEN     

And now IPv6-only clients can connect to 2a00:8640:1::224:36ff:feef:1d89 and IPv4-only clients can connect to 37.77.56.75. Dual stack clients can choose which protocol they want to use.

于 2013-05-26T20:54:15.313 回答