Created
August 2, 2012 00:30
-
-
Save digitalresistor/3231892 to your computer and use it in GitHub Desktop.
Verify sanity with regards to bind() on sockets when receiving info from getaddrinfo(). AF_INET vs AF_INET6.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* There is inconsistent behaviour regarding the way various operating | |
* systems use IPV6_V6ONLY by default and interactions with bind() to bind a | |
* wildcard socket (as in all available addresses) on an AF_INET6 (IPv6) | |
* socket. | |
* | |
* getaddrinfo() returns two results, the first one being AF_INET and the | |
* second being AF_INET6 (Unless you are on an OS that prefers IPv6, I'm | |
* looking at you OI). We create a socket, bind, and listen on AF_INET first. | |
* At that point we try to create a socket, bind and then listen on AF_INET6. | |
* This is where the inconsistencies become apparent. | |
* | |
* Linux if we ask for AF_UNSPEC we get handed back an IPv4 INADDR_ANY first, | |
* when we bind to that and then get an IPv6 IN6ADDR_ANY_INIT and try to bind | |
* we get a bind() failure because it is also attempting to bind on IPv4. This | |
* is rather unexpected to me, I created the socket on IPv6, it should let me | |
* bind. Even if that means that it needs to not bind on IPv4. This failure | |
* mode does not exist on any other operating systems tested. Linux is the only | |
* one that will return an error on an attempt to bind. | |
* | |
* Mac OS X will let you bind on IPv4, and then when you bind on IPv6 | |
* it will bind on both IPv6 AND IPv4. Netstat output: | |
* | |
* tcp46 0 0 *.7020 *.* LISTEN | |
* tcp4 0 0 *.7020 *.* LISTEN | |
* | |
* FreeBSD has disabled IPv4 mapped on IPv6 addresses for a while now, | |
* so this feature would have to be turned on by a system | |
* administrator. The default is sane and doesn't result in a failure. | |
* What should be determined is what happens when this IPv4 mapped on | |
* IPv6 addresses is turned on, unfortunately I don't have a test | |
* machine to attempt this experiment. | |
* | |
* OpenIndiana has the same sort of mode as Mac OS X, although its | |
* getaddrinfo() will actually return the IPv6 based address first, and | |
* the IPv4 based address second, but both bind's will complete without | |
* issues, and netstat output will look similar to this: | |
* | |
* TCP: IPv6 | |
* Local Address Remote Address Swind Send-Q Rwind Recv-Q State If | |
* -------------------- -------------------- ----- ------ ------ ------ ----------- ----- | |
* *.7020 *.* 0 0 128000 0 LISTEN | |
* | |
* | |
* TCP: IPv4 | |
* Local Address Remote Address Swind Send-Q Rwind Recv-Q State | |
* -------------------- -------------------- ----- ------ ------ ------ ----------- | |
* *.7020 *.* 0 0 128000 0 LISTEN | |
* *.7020 *.* 0 0 128000 0 LISTEN | |
* | |
* The question that still also needs to be answered is which one of | |
* the listening sockets can accept() a connection, whether it is the | |
* IPv4 mapped on IPv6 that gets the connection, or whether the OS | |
* prefers to use the IPv4 socket. | |
* | |
* Oh, and I am so glad that we (Linux, FreeBSD, Mac OS X, OpenIndiana) could | |
* all standardise our address family numbers/defines, same applies for socket | |
* type. Luckily protocol isn't defined by the OS creators. Having the same | |
* number across multiple OS's would make it much simpler to debug across | |
* multiple platforms. C'est la vie! | |
* | |
* Compile: | |
* | |
* clang getaddrinfo.c -o getaddrinfo | |
* | |
* Or if you are testing for failure modes: | |
* | |
* clang getaddrinfo.c -o getaddrinfo -DNOIPV6ONLY | |
* | |
* On OpenIndiana you may also need to link in the sockets library: | |
* | |
* cc getaddrinfo.c -o getaddrinfo -lsockets | |
* | |
* Run: | |
* | |
* ./getaddrinfo | |
* | |
* Sample output (OpenIndiana): | |
* | |
* Address family: AF_INET6 (26) Socket type: SOCK_STREAM (2) Protocol: tcp (6) | |
* Address family: AF_INET (2) Socket type: SOCK_STREAM (2) Protocol: tcp (6) | |
* Waiting for user input to close all sockets and end... <Enter> | |
* closing socket: 4 | |
* closing socket: 5 | |
*/ | |
#include <stdio.h> | |
#include <string.h> | |
#include <unistd.h> | |
#include <sys/types.h> | |
#include <sys/socket.h> | |
#include <netdb.h> | |
#include <errno.h> | |
#include <stdlib.h> | |
char * family_type(int af) { | |
switch (af) { | |
case AF_INET: | |
return "AF_INET"; | |
case AF_INET6: | |
return "AF_INET6"; | |
default: | |
return "UNKNOWN"; | |
} | |
} | |
struct socket_links { | |
struct socket_links *next; | |
int sockfd; | |
}; | |
int main() { | |
struct addrinfo hints, *addrlist, *addr; | |
struct socket_links *head, *current; | |
// Thanks great big OS for cleaning up after me ... | |
current = head = malloc(sizeof(struct socket_links)); | |
current->next = 0; | |
memset(&hints, 0, sizeof(hints)); | |
// Ask for TCP | |
hints.ai_socktype = SOCK_STREAM; | |
// Any family works for us ... | |
hints.ai_family = AF_UNSPEC; | |
// Set some hints | |
hints.ai_flags = | |
AI_PASSIVE | // We want to use this with bind | |
AI_NUMERICSERV | // We only pass in numeric port numbers | |
AI_NUMERICHOST; // We only pass in numeric IP addresses | |
int rv; | |
if ((rv = getaddrinfo(0, "7020", &hints, &addrlist)) != 0) { | |
fprintf(stderr, "getaddrinfo: %s", gai_strerror(rv)); | |
return 1; | |
} | |
for (addr = addrlist; addr != 0; addr = addr->ai_next) { | |
struct protoent *proto = getprotobynumber(addr->ai_protocol); | |
printf("Address family: %s (%d)\tSocket type: %s (%d)\tProtocol: %s (%d)\n", | |
family_type(addr->ai_family), | |
addr->ai_family, | |
addr->ai_socktype == SOCK_STREAM ? "SOCK_STREAM" : "other", | |
addr->ai_socktype, | |
proto->p_name, | |
addr->ai_protocol); | |
int sockfd; | |
if ((sockfd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol)) == -1) { | |
fprintf(stderr, "socket: %s\n", strerror(errno)); | |
continue; | |
} | |
int yes = 1; | |
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { | |
close(sockfd); | |
fprintf(stderr, "setsockopt: %s\n", strerror(errno)); | |
continue; | |
} | |
#ifndef NOIPV6ONLY | |
// If the socket is AF_INET6 | |
if (addr->ai_family == AF_INET6) { | |
// Set the socket to be IPv6 only and don't allow IPv4 traffic on this socket | |
// | |
// Now we can bind() without issues on Linux | |
if (setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(int)) == -1) { | |
close(sockfd); | |
fprintf(stderr, "setsockopt: %s IPV6_V6ONLY\n", strerror(errno)); | |
continue; | |
} | |
} | |
#endif | |
if (bind(sockfd, addr->ai_addr, addr->ai_addrlen) == -1) { | |
close(sockfd); | |
fprintf(stderr, "bind: %s\n", strerror(errno)); | |
continue; | |
} | |
if (listen(sockfd, 20) == -1) { | |
close(sockfd); | |
fprintf(stderr, "listen: %s\n", strerror(errno)); | |
continue; | |
} | |
current->sockfd = sockfd; | |
current->next = malloc(sizeof(struct socket_links)); | |
current = current->next; | |
current->next = 0; | |
current->sockfd = 0; | |
} | |
printf("Waiting for user input to close all sockets and end..."); | |
getchar(); | |
for (current = head; current->sockfd != 0; current = current->next) { | |
printf("closing socket: %d\n", current->sockfd); | |
close(current->sockfd); | |
} | |
freeaddrinfo(addrlist); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment