[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Re: broken IPv6 code (attached exampled app)



Hi all

Sorry for the crossposting, but I think it is important to discuss if the Linux
kernel and/or glibc is really wrong here or if applications have to take care
about this issue for themselves. Unfortunately I do not know how other vendors
implement this. A previous mail on these lists said that vendors did not agree
to specify it, but I have no idea how they differ. However, *BSD and Linux
differ anyway, making portable coding difficult.

Brian May wrote:
> If an error is returned from an IPv4 bind operation, how is the
> application to know if:
> 
> a) the port is already in use by another application
> 
> b) the IPv6 bind has also implies a bind to the IPv4 port.
> 
> which is really dependent on the OS being used.
Yes, and this is bad. Because when the application should work on Linux, it
might open a security hole on another system (and no, the application should not
check if it is running on Linux or any other system implementing it this way
.....).
 
> I think there are two solutions:
> 
> 1. getaddrinfo should not return IPv4 addresses if an IPv6 address exist,
> since using the IPv6 address implies usage of the IPv4 address.
> 
> 2. IPv4 and IPv6 protocols kept completely separate.
> 
> Either one or the other, it doesn't really matter, either way it is
> possible to create portable applications that aren't broken. The
> choice could vary depending on operating system too, I think.
Completely right here. I think for most applications it doesn't matter which
solution the OS takes. Maybe (2) would be the better solution because it allows
to listen *only* on IPv6 without having to listen on every IPv6 interface
exlicitely.
If Linux/glibc is wrong here, who should be contacted ? [Read: Who is easier to
convince that something should be changed - the kernel IPv6 maintainer(s) or the
glibc maintainers ?] Or is it a glibc problem at all (could (2) be done in glibc
without changes to the kernel part) ?

> (then again, I only have Linux 2.2.18 - does 2.4.0x behave any
> differently?)
No, 2.4.0-prerelease behaves exactly the same, I have just tried it with a small
testing application.

Attached is my simple IPv6 client/server testing application (uses C++ just for
convenience) that checks for this failure. It works on Linux 2.4.0-prerelease
and glibc 2.2 (from the libc6_2.2-8 Debian package), but at the moment I have no
access to a *BSD system to test it on. I think it opens the discussed security
hole by ignoring a bind() failure on the IPv4 port. Another application might be
able to bind to the socket without the IPv6 application noticing it. But this is
only speculation, I have no test case for this. 
Please note that I have written this application to be protocol independant to
the best of my knowledge (the small bits that I collected on the net helped a
bit), but it is my first IPv6 application, so it might not be ideal as an
example. Another thing to note is that this check for the failing IPv4 bind is
really the ONLY OCCURANCE of protocol dependant code, and I would like it to go
away. If anybody has a hint on how this could be done with the current
Linux/glibc situation, please let me know. I appreciate every suggestion and bug
report very much.

best greets,
Rene
/***************************************************************************
                          main.c  -  simple UDP client/server application
                             -------------------
    begin                : Wed Jan 3 2001
    copyright            : (C) 2001 by Rene Mayrhofer
    email                : rene.mayrhofer@vianova.at
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

/* This file implements a simple client / server application for transferring
   the current system time from the client to the server. It uses simple UDP
   packets because TCP would be too much overhead in this simple case. The
   client will send an UDP packet containing its current system time as string
   (dependent on the system locale) to the server, which will answer with the
   same packet as acknowledgement. If the client does not receive an answer
   from the server within 1 second, it will retry for a mxaimum of 5 times
   before giving up. The server will only wait for one UDP packet containing
   a timestamp and will answer this packet before exiting immediately
   afterwards.
   Both the client and the server are implemented in a protocol-independant
   manner so that they will use IPv4 and/or IPv6, whatever is available.
*/

/* standard stuff */
#include <stdio.h>
#include <stdlib.h>
#define __USE_GNU	/* this is for strnlen() */
#include <string.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
/* for getopt */
#include <getopt.h>
/* for network operations */
#include <netinet/in.h>
#include <sys/socket.h>
#include <netdb.h>
/* C++ collections */
#include <vector>

#include <arpa/inet.h>

#define MODE_UNSPECIFIED 0
#define MODE_CLIENT 1
#define MODE_SERVER 2
#define PORT_UNSPECIFIED 0

#define MAX_RETRIES 5
#define TIMEOUT 1

#define MAXLEN_HOSTNAME 256
#define MAXLEN_PORTNAME 30
#define MAXLEN_PORTSTR 6
#define MAXLEN_MESSAGE 128

static option long_opts[] = {
	{"help",		no_argument,		NULL,	'h'},
	{"verbose",		no_argument,		NULL,	'v'},
	{"mode",		required_argument,	NULL,	'm'},
	{"port",		required_argument,	NULL,	'p'},
	{"server",		required_argument,	NULL,	's'},
	{0, 0, 0, 0}
};

void error(char *errstr, int errnum);
void print_usage();
void start_server(short port, bool verbose);
void start_client(char* servername, short port, bool verbose);

int main(int argc, char *argv[]) {
	bool verbose = false;
	char c, *temp, *servername = NULL;
	short port = PORT_UNSPECIFIED, mode = MODE_UNSPECIFIED;

	/* check the parameters
	   (including some security checks for buffer overflow attacks) */
	while ((c = getopt_long(argc, argv, "hvm:p:s:", long_opts, NULL)) != -1) {
		switch (c) {
			/* print some help and exit */
			case 'h':	print_usage();
						exit(0);
			/* verbose mode: print more information */
			case 'v':	verbose = true;
						break;
			/* mode parameter: validate and set the mode variable */
			case 'm':	if (strnlen(optarg, 2) > 1 ||
								(optarg[0] != 'c' && optarg[0] != 's'))
							error("Please specify 'c' or 's' as mode", 1);
						else if (optarg[0] == 'c')
							mode = MODE_CLIENT;
						else
							mode = MODE_SERVER;
						break;
			/* port parameter: set the port variable */
			case 'p':	if (strnlen(optarg, 6) > 5)
							error("The port number must be less than "\
								   "65536", 2);
						port = strtol(optarg, &temp, 10);
						if (*temp != '\0')
							error("The given port value is not a valid "\
								   "number.", 3);
						break;
			case 's':	if (strnlen(optarg, 256) > 255)
							error("The servername must be shorter then 256 "\
								   "characters.", 4);
						servername = strdup(optarg);
						if (servername == NULL)
							error("Unable to allocate memory.", 100);
						break;
		};
	}

	if (mode == MODE_UNSPECIFIED)
		error("The mode must be specified.", 10);
	if (port == PORT_UNSPECIFIED)
		error("The port must be specified.", 11);

	/* now switch between client and server mode */
	if (mode == MODE_CLIENT) {
		if (servername == NULL)
			error("The server must be specified.", 12);
		
		printf("Starting in client mode.\n");
		start_client(servername, port, verbose);
		printf("Leaving client mode.\n");
	}
	else {
		printf("Starting in server mode.\n");
		start_server(port, verbose);
		printf("Leaving server mode.\n");
	}	

	/* clean up */
	if (servername != NULL)
		free(servername);
	exit(0);
}

void print_usage() {
	printf("Usage: sendtime <options>\n\n");
	printf("Options:\n");
	printf("\t-h, --help\tthis help\n");
	printf("\t-v, --verbose\tbe more verbose\n");
	printf("\t-m, --mode\teither 'c' for client or 's' for server\n");
	printf("\t-p, --port\tfor client mode: port number to connect to\n");
	printf("\t\t\tfor server mode: port number to listen on\n");
	printf("\t-s, --server\tfor client mode: server name or IP address " \
		   "to connect to\n\n");
	printf("The mode and port options have to be specified, the server " \
		   "option is needed\n");
	printf("only for for client mode and ignored otherwise\n");
}

void error(char *errstr, int errnum) {
	fprintf(stderr, "Error: %s\n", errstr);
	exit(errnum);
}

void start_server(short port, bool verbose) {
	char portstr[MAXLEN_PORTSTR],
		hostname[MAXLEN_HOSTNAME],
		portname[MAXLEN_PORTNAME],
		msg[MAXLEN_MESSAGE];
	int errnum, sockfd, maxfd, size;
	vector<int> listeningSockets;
	vector<int>::size_type i;
	addrinfo hints, *addrinfo, *addrinfo2;
	sockaddr_storage clientSockaddr;
	socklen_t clientSocklen;
	fd_set readfds;

	/* Since this has to be protocol-independant, use getaddrinfo to get
	   every possible local network interface and protocol. */
	/* fill the hints structure for getaddrinfo */
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = IPPROTO_UDP;
	hints.ai_flags = AI_PASSIVE;
	/* Transform port number back to a string. It is overhead, but safe. */
	sprintf(portstr, "%d", port);
	if ((errnum = getaddrinfo(NULL, portstr, &hints, &addrinfo)) != 0) {
		error(gai_strerror(errnum), 104);
	}
	else {
		/* getaddrinfo returns a list of addresses, so go over all of
		   them and bind to everyone */
		for (addrinfo2=addrinfo; addrinfo != NULL;
				addrinfo=addrinfo->ai_next) {
			if (getnameinfo(addrinfo->ai_addr, addrinfo->ai_addrlen,
					hostname, MAXLEN_HOSTNAME, portname, MAXLEN_PORTNAME,
					NI_NUMERICHOST | NI_NUMERICSERV) != 0)
				error(strerror(errno), 105);
			if (verbose)
				printf("Trying address family %d, socket type %d, " \
					   "protocol %d, address %s, port %s\n",
   		        	   addrinfo->ai_family, addrinfo->ai_socktype,
					   addrinfo->ai_protocol, hostname, portname);

			/* try to connect and bind to the socket */
           	if ((sockfd = socket(addrinfo->ai_family, addrinfo->ai_socktype,
					addrinfo->ai_protocol)) < 0) {
				/* an error during creating the socket can be ignored if
				   it is error code 93 = "Protocol not supported", because
				   getaddrinfo also return address family PF_LOCAL when
				   called with PF_UNSPEC, and we don't want to bind to
				   a local socket anyway (which is the reason for this
				   error) */
				if (errno == 93) {
					if (verbose)
						printf("Unable to create socket, ignoring.\n");
					continue;
				}
				else
					error(strerror(errno), 106);
   	        }
       	    if (bind(sockfd, addrinfo->ai_addr,	addrinfo->ai_addrlen)
					!= 0) {
				close(sockfd);
				/* an error during binding can be ignored if it is error
				   code 98 = "Address already in use" and the address
				   family is IPv4, because under Linux binding to an IPv6
				   socket automatically binds to the IPv4 port too */
				/* Note: This is the only occurance of any protocol-
				   dependant code and it is only needed because the RFC
				   is not specific enough on the behaviour of binding to
				   IPv6 and IPv4 ports. On FreeBSD and OpenBSD this case
				   will not occur, on Linux it does. These are the
				   different interpretations of the RFC in the wild.... */
				if (errno == 98 && addrinfo->ai_family == PF_INET) {
					if (verbose)
						printf("Already bound to it, ignoring.\n");
					continue;
				}
				else
					error(strerror(errno), 107);
   	        }
			printf("Listening on address %s, port %s.\n",
				hostname, portname);
			/* remember this socket (it is still open) before trying
			   the next one */
			listeningSockets.push_back(sockfd);
        }
		freeaddrinfo(addrinfo2);
	}

	/* now wait for packets on all listening sockets (if there are any
	   open sockets) */
	if (! listeningSockets.empty()) {
		FD_ZERO(&readfds);
		maxfd=-1;
		for (i=0; i<listeningSockets.size(); i++) {
			FD_SET(listeningSockets[i], &readfds);
			/* remember the maximum file descriptor for select() */
			if (listeningSockets[i] > maxfd)
				maxfd = listeningSockets[i];
		}
		if (select(maxfd + 1, &readfds, NULL, NULL,
			NULL /* no timeout, wait indefinitely */
			) <= 0)
			error("Internal error on select()", 200);

		/* search for the file descriptor that caused select() to terminate */
		i=0;
		while (i<listeningSockets.size() &&
			! FD_ISSET(listeningSockets[i], &readfds)) i++;
		if (FD_ISSET(listeningSockets[i], &readfds)) {
			sockfd = listeningSockets[i];
			size = recvfrom(sockfd, msg, MAXLEN_MESSAGE, 0,
					(sockaddr *) &clientSockaddr, &clientSocklen);
			if (size == -1)
				error(strerror(errno), 108);

			/* now print the timestamp from the client and answer the packet
			   by simply sending the contents back */
			if (getnameinfo((sockaddr *) &clientSockaddr, clientSocklen,
					hostname, MAXLEN_HOSTNAME, portname, MAXLEN_PORTNAME,
					NI_NUMERICHOST | NI_NUMERICSERV) != 0)
				error(strerror(errno), 105);
			if (msg[size-1] != 0) {
				printf("Warning: received a message that is not " \
					   "null-terminated, correcting.\n");
				msg[size-1] = 0;
			}
			printf("Received %d bytes from address %s, port %s: '%s'\n",
				size, hostname, portname, msg);
			errnum = sendto(sockfd, msg, size, 0,
				(sockaddr *) &clientSockaddr, clientSocklen);
			if (errnum == -1)
				error(strerror(errno), 109);
			if (errnum  != size)
				error("Short send during replying packet.\n", 110);
		}

		/* clean up */
		for (i=0; i<listeningSockets.size(); i++)
			close(listeningSockets[i]);
	}
}

void start_client(char* servername, short port, bool verbose) {
	char portstr[MAXLEN_PORTSTR],
		hostname[MAXLEN_HOSTNAME],
		portname[MAXLEN_PORTNAME],
		msg[MAXLEN_MESSAGE],
		ackmsg[MAXLEN_MESSAGE];
	bool done = false;
	short retry, msgsize;
	int errnum, sockfd, size, acksize;
	addrinfo hints, *addrinfo, *addrinfo2;
	sockaddr_storage serverSockaddr;
	socklen_t serverSocklen;
	timeval readtimeout;
	fd_set readfds;
	time_t localtime;

	/* generate the message to be sent */
	time(&localtime);
	strncpy(msg, ctime(&localtime), MAXLEN_MESSAGE);
	msgsize = strnlen(msg, MAXLEN_MESSAGE - 1);
	/* this cuts the trailing '\n', and the message size will also be correct */
	msg[msgsize-1] = 0;
	printf("Current time is %s\n", msg);

	/* fill the hints structure for getaddrinfo */
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = IPPROTO_UDP;
	/* Transform port number back to a string. It is overhead, but safe. */
	sprintf(portstr, "%d", port);
	/* get the server address(es) */
	if ((errnum = getaddrinfo(servername, portstr, &hints, &addrinfo)) != 0) {
		error(gai_strerror(errnum), 110);
	}
	else {
		/* getaddrinfo returns a list of addresses, so go over all of
		   them and connect to the first one that works */
		for (addrinfo2=addrinfo; addrinfo != NULL && !done;
				addrinfo=addrinfo->ai_next) {
			if(getnameinfo(addrinfo->ai_addr, addrinfo->ai_addrlen,
				hostname, MAXLEN_HOSTNAME, portname, MAXLEN_PORTNAME,
				NI_NUMERICHOST | NI_NUMERICSERV) != 0)
				error(strerror(errno), 105);
			if (verbose)
				printf("Trying to connect to address family %d, socket type " \
					    "%d, protocol %d, address %s, port %s\n",
	   		        addrinfo->ai_family, addrinfo->ai_socktype,
					addrinfo->ai_protocol, hostname, portname);

			sockfd = socket(addrinfo->ai_family, addrinfo->ai_socktype,
					addrinfo->ai_protocol);
			/* if the socket can not be created - just skip and try next */
			if (sockfd >= 0) {
				/* retry for a number of MAX_RETRIES before giving up on this
				   address and trying the next one */
				for (retry=0; retry<=MAX_RETRIES && !done; retry++) {
						size = sendto(sockfd, msg, msgsize, 0,
							addrinfo->ai_addr, addrinfo->ai_addrlen);
						if (size != msgsize)
							error("Short write during sending", 111);
						
						/* wait for the reply */
						FD_ZERO(&readfds);
						FD_SET(sockfd, &readfds);
						readtimeout.tv_sec = TIMEOUT;
						readtimeout.tv_usec = 0;
						errnum = select(sockfd + 1, &readfds, NULL, NULL,
							&readtimeout);
						if (errnum == -1)
							error(strerror(errno), 112);
						else if (errnum == 1) {
							/* read the reply */
							acksize = recvfrom(sockfd, ackmsg, MAXLEN_MESSAGE,
								0, (sockaddr *) &serverSockaddr,
								&serverSocklen);
							if (acksize == size &&
								strncmp(ackmsg, msg, size) == 0) {
								printf("Successfully sent timestamp to " \
									"address %s, port %s and received " \
									"acknowledge.\n", hostname, portname);
								done = true;
							}
							else {
								printf("Received invalid reply from " \
									"address %s, port %s.\n",
									hostname, portname);
							}
						}
						else if (verbose)
							printf("Timeout on waiting for acknowledge.\n");
				}
				if (!done)
					printf("Received no acknowledge from address %s, " \
						"port %s.\n", hostname, portname);
			}
			else if(verbose)
				printf("Unable to create socket for connection.\n");
		}
		if (!done)
			printf("Received no acknowledge at all.\n");
	}		
}

Reply to: