Universidad Técnica Federico Santa María INF-354 Taller de Redes de Computadores Manejo de sockets IPv6 Autores: Alonso Sandoval Hernán Vargas Profesor: Javier Cañas 21 de noviembre de 2014
Índice 1. Resumen 2 2. Acerca de la aplicación 2 2.1. Cliente..................................................... 2 2.2. Servidor.................................................... 3 3. Migrando aplicaciones 3 3.1. Funciones estándar de sockets........................................ 3 3.2. Estructura de direcciones.......................................... 4 3.3. Resolución DNS............................................... 4 4. Explorando el código fuente 5 4.1. Sockets.................................................... 5 4.2. Estructura addrinfo............................................. 7 5. Referencias 9 1
1. Resumen Nuestro proyecto consiste en 2 aplicaciones (un cliente y un servidor) las cuales interactúan tanto en el protocolo IPv4 1 como en IPv6 2. En el siguiente documento presentaremos los detalles de nuestro proyecto, mostrando tanto la parte usuario (formas de uso) como la que interesa al estudiante (Código, librerias, etc.). Nuestro objetivo es poder manipular la nueva estructura que compone el protocolo IPv6 a través de la librería de sockets en C [1], permitiendonos hacer pruebas para ver las principales diferencias con su predecesor el protocolo IPv4. Para más detalles del proyecto, visitar el trac del curso [2]. 2. Acerca de la aplicación Como dijimos anteriormente nuestro proyecto comunica dos aplicaciones siguiendo la estructura de cliente y servidor, estando de preferencia en máquinas distintas de manera que podamos hacer las pruebas pertinentes en un ambiente lo más realista posible. A continuación presentamos la forma de usar ambas aplicaciones, la cuales se obtienen al compilar el código fuente. 2.1. Cliente Las opciones con la aplicación cliente son: Modo de empleo: $./client -4 [OPCIONES] (Para IPv4.) $./client -6 [OPCIONES] (Para IPv6.) Las opciones son: Ejemplos: $ -a, address (Define explícitamente la dirección.) $ -p, port (Define explícitamente el puerto.) $./client -6 -a:: -p4021 $./client -6 address=:: port=4021 Existe una configuración por defecto la cual es guardada en el archivo config ubicado en el directorio raíz del proyecto. Una vez ingresado los parámetros, el cliente intentará conectar con el servidor que debiese estar en la máquina que posee la dirección otorgada por el usuario, si lo logra comenzará la sesión permitiendo el paso de mensajes. Los mensajes enviados son analizados por el servidor y se envía una respuesta acorde a ellos. Si el mensaje no está en la lista de funciones establecidas, el servidor responderá lo mismo que se le envió (en este caso, lo que envía el cliente). El servidor puede establecer la comunicación entre dos clientes, lo cual a su vez permite la comunicación entre distintos protocolos. Además, podemos enviar archivos los cuales son guardados en la carpeta del cliente Las funciones disponibles son: list: Muestra una lista de los clientes conectados mostrando su alias e IP. wait: Pone al cliente en modo espera de otras conexiones. linkto: Conecta al cliente con alias IPvX:Y, donde X es la versión del protocolo e Y el alias del cliente. Notar que dicho cliente debe estar en modo wait. Ej: > linkto IPv6:5. fileto: Envía un archivo al cliente de alias Y. Ej: > fileto IPv6:5..filesrc filedst. close, quit, exit: Cierra el socket conectado con el cliente. 1 Internet Protocol version 4 2 Internet Protocol version 6 2
2.2. Servidor Por parte del servidor no existen muchas opciones dado que todo lo editable debiese estar en el archivo de configuración, el cual como dijimos anteriormente se encuentra en el directorio raíz. Modo de empleo: $./server Como dijimos, el servidor lee la configuración de puertos desde el archivo de configuración. Internamente selecciona una IP disponible por medio de IP ANY tanto para IPv4 como para IPv6, luego lanza en threads [3] los dos sockets escucha 3. Mientras está escuchando muestra las conexiones entrantes y los mensajes recibidos. 3. Migrando aplicaciones Al momento de pensar o repensar nuestras aplicaciones a modo que funcione o sea compatible con el nuevo protocolo IPv6 nos encontramos con que el principal problema es el tamaño de direcciones, no por su tamaño en bytes sino por su formato. Si bien un socket es un nivel de abstracción donde no utilizamos la dirección directamente, es necesario proporcionarla para la creación de este. 3.1. Funciones estándar de sockets Para C la biblioteca socket.h provee las funciones necesarias para la creación de sockets. Tenemos las siguientes funciones: int socket (int domain, int type, int protocol); int bind (int fd, struct sockaddr* addr, int addrlen); int listen (int fd, int backlog); int connect (int fd, struct sockaddr* addr, int addrlen); int accept (int fd, void* recv addr, int* addrlen); int send (int fd, void* msg, int len, int flags); int recv (int fd, void* msg, int len, int flags); int shutdown (int fd, int level); Para cada una de estas funciones destacamos: La función socket retorna un file descriptor (fd). Las funciones send y recv retornan la cantidad de bytes enviados/recibidos. Todas las demás funciones retornan 0 en caso de ejecutarse correctamente y -1 en caso contrario. Algunas definiciones de los datos: domain: Dominio del protocolo, en nuestro caso AF INET para IPv4 y AF INET6 para IPv6. type: Tipo de conexión, SOCK STREAM para flujos o SOCK DIAGRAM para datagramas. protocol: Protocolo de conexión, debe ser congruente al tipo. Si el tipo de conexión solo tiene un protocolo asociado (ej: TCP) se puede poner un 0. fd: File descriptor del socket objetivo. addr, recv addr: Estructura de dirección. Depende del protocolo. addrlen: Largo de la estructura de dirección. 3 Sockets a la espera de recibir una conexión entrante 3
backlog: Cantidad de conexiones en espera que podemos tener simultáneamente. msg: Buffer donde guardar mensajes a enviar o recibidos. len: Largo del mensaje a enviar o máximo del mensaje a recibir. level: Nivel de apagado, 0 prohíbe la recepción, 1 prohíbe la emisión y 2 prohíbe ambos. Cabe destacar que como los sockets, una vez son creados, son manejados prácticamente como cualquier otro fichero por el sistema operativo, podemos utilizar funciones como read, write y close en vez de rcv, send y shutdown respectivamente. Como podemos notar, la única dependencia de estas funciones con IPv4 o IPv6 van por parte de las estructuras de dirección. Pero las funciones aceptan la estructura general: sockaddr y no es específica, así, el problema de migración no será en los sockets mismos, si no más bien en las estructuras. 3.2. Estructura de direcciones Las estructuras de direcciones IPv4 e IPv6 están definidas de la siguiente forma: Estructura IPv4: struct in_addr{ unsigned long s_addr; /* 32b para la dirección. */ struct sockaddr_in{ short sin_family; /* AF_INET */ unsigned short sin_port; /* Puerto en formato network. */ struct in_addr sin_addr; char sin_zero[8]; Estructura IPv6: struct in6_addr { u_int8_t s6_addr[16]; /* 128b para la dirección. */ struct sockaddr_in6{ u_char sin6_len; /* Largo de esta estructura. */ u_char sin6_family; /* AF_INET6 */ u_int16m_t sin6_port; /* Puerto en formato network. */ u_int32m_t sin6_flowinfo; struct in6_addr sin6_addr; Inmediatamente notamos que el cambio fundamental entre estas estructuras se encuentra en la longitud de la dirección IP. Además se han quitado los ceros de sockaddr in (eran para completar el bloque) y se ha agregado el largo de la estructura y flowinfo. Nota: Las estructuras descritas anteriormente guardan la información en formato (binario), para ello debemos utilizar las siguientes funciones de conversión al guarda/leer los puertos: int network port = htons(int human readabl port); int human readable port = ntohs(int network port); 3.3. Resolución DNS Para poder obtener la(s) direccion(es) IP rela(es) de un servidor debemos hacer la consulta al DNS, en C tenemos la biblioteca netdb.h que provee funciones y estructuras que facilitan esta tarea: 4
#include <netdb.h> /* Estructura hostent para resolver direcciones. */ struct hostent{ char* h_name; /* Nombre del host. */ char** h_aliases; /* Nombres alternativos del host. */ int h_addrtype; /* Tipo de dirección (AF_INET o AF_INET6). */ int h_length; /* Longitud, en bytes, de la dirección. */ char** h_addr_list; /* Puntero a las direcciones del host. */ char* h_addr; /* Primera dirección del host (h_addr_list[0]). */ /* Funciones */ struct hostent* gethostbyname(const char* name); struct hostent* gethostbyname2(const char* name, int af); struct hostent* gethostbyaddr(const void* addr, socklen_t length, int format); La función gethostbyname(name) obtiene las direcciones IP de algún servidor name, por ejemplo name = www.google.cl obtendrá las direcciones de google. La función gethostbyname2(name, af) es igual a gethostbyname(...) solo que filtra las direcciones dependiendo de la variable af (AF INET o AF INET6, en nuestro caso). La función gethostbyaddr(addr, length, format) hace lo contrario a las instrucciones anteriores. Se le suministra addr que es una estructura de direcciones, length el largo de dicha estructura y af el tipo de dirección y determina los demás campos de la estructura conforme a ello. 4. Explorando el código fuente A continuación se presenta el código básico para la creación de sockets en IPv4: 4.1. Sockets Sockets para IPv4 #include <netinet/in.h> /* Estructuras y constantes de dirección. */ #include <sys/socket.h> /* Funciones para el manejo de sockets. */ #include <netdb.h> /* Estructura y funciones para DNS. */ #define SERVER 0 #define CLIENT 1 /* == Parámetros == */ char direccion[len] = "www.ejemplo.com"; /* Puede ser una dirección ip. */ int puerto = 4040; /* Puerto al que nos conectaremos. */ int c = TIPO_APP; /* TIPO_APP será SERVER o CLIENT. */ /* == Variables == */ int mi_socket; /* Entero para guardar el fd del socket. */ struct sockaddr_in in; /* Estructura descrita en la sección anterior. */ struct hostent *he; /* Donde guardaremos el resultado de la consulta DNS. */ /* == Configuración de la dirección == */ menset(&in, 0, sizeof(in)); /* Llenamos la estructura de 0 s y de paso sin_zero. */ in.sin_family = AF_INET; /* Familia IPv4. */ in.sin_port = htons(puerto); /* Puerto en formato network. */ /* Servidor: seleccionamos cualquier IP disponible, */ 5
in.sin_addr.s_addr = INADDR_ANY; /* Cliente: debemos resolver la dirección por medio del DNS, */ else if(c == CLIENT){ he = gethostbyname(direccion); /* Consulta al DNS. */ in.sin_addr = *( (struct in_addr *) he->h_addr); /* Guardamos la dirección en la estructura. */ /* == Trabajo con sockets == */ mi_socket = socket(af_inet, SOCK_STREAM, 0); /* Creación del socket. */ /* Servidor: debemos ponernos en modo escucha, */ bind(mi_socket, (struct sockaddr *) &in, sizeof(in)); /* Linkeamos nuestra dirección y el puerto. */ listen(mi_socket, BACKLOG); /* Modo escucha. */ /* Variables para guardar clientes. */ struct sockaddr_in cliente; int socket_entrante, len_cl = sizeof(struct sockaddr_in); socket_entrante = accept(mi_socket, (struct sockaddr *) &cliente, &len_cl); /* Con esto tendremos una conxion con cliente por medio de socket_entante. */ /* cliente: basta hacer connect, este internamente hace el bind. */ else if(c == CLIENT){ connect(mi_socket, (struct sockaddr *) &in, sizeof(in)); Sockets para IPv6 #include <netinet/in.h> /* Estructuras y constantes de dirección. */ #include <sys/socket.h> /* Funciones para el manejo de sockets. */ #include <netdb.h> /* Estructura y funciones para DNS. */ #define SERVER 0 #define CLIENT 1 /* == Parámetros == */ char direccion[len] = "www.ejemplo.com"; /* Puede ser una dirección ip. */ int puerto = 4040; /* Puerto al que nos conectaremos. */ int c = TIPO_APP; /* TIPO_APP será SERVER o CLIENT. */ /* == Variables == */ int mi_socket; /* Entero para guardar el fd del socket. */ struct sockaddr_in6 in; /* Estructura descrita en la sección anterior. */ struct hostent *he; /* Donde guardaremos el resultado de la consulta DNS. */ /* == Configuración de la dirección == */ in.sin6_family = AF_INET6; /* Familia IPv6. */ in.sin6_port = htons(puerto); /* Puerto en formato network. */ in.sin6_flowinfo = 0; /* Servidor: seleccionamos cualquier IP disponible, */ in.sin6_addr = in6addr_any; /* Cliente: debemos resolver la dirección por medio del DNS, */ else if (c == CLIENT){ he = gethostbyname2(direccion, AF_INET6); /* Consulta al DNS. */ memcpy((char *) &in.sin6_addr, he->h_addr, he->h_length); /* Guardamos la dirección en la estructura. */ /* == Trabajo con sockets == */ 6
mi_socket = socket(af_inet6, SOCK_STREAM, 0); /* Creación del socket. */ /* Servidor: debemos ponernos en modo escucha, */ bind(mi_socket, (struct sockaddr *) &in, sizeof(in)); /* Linkeamos nuestra dirección y el puerto. */ listen(mi_socket, BACKLOG); /* Modo escucha. */ /* Variables para guardar clientes. */ struct sockaddr_in6 cliente; int socket_entrante, len_cl = sizeof(struct sockaddr_in6); socket_entrante = accept(mi_socket, (struct sockaddr *) &cliente, &len_cl); /* Con esto tendremos una conxion con cliente por medio de socket_entante. */ /* Cliente: basta hacer connect, este internamente hace el bind. */ else if(c == CLIENT){ connect(mi_socket, (struct sockaddr *) &in, sizeof(in)); Cuales son las diferencias entre ambos códigos?: struct sockaddr in in; struct sockaddr in6 in; menset(&in, 0, sizeof(in)); in.sin6 flowinfo = 0; in.sin family = AF INET; in.sin6 family = AF INET6; in.sin port = htons(puerto); in.sin6 port = htons(puerto); in.sin addr.s addr = INADDR ANY; in.sin6 addr = in6addr any; he = gethostbyname(direccion); he = gethostbyname2(direccion, AF INET6); in.sin addr = *( (struct in addr *) he->h addr); memcpy((char *) &in.sin6 addr, he->h addr, he->h length); mi socket = socket(af INET, SOCK STREAM, O); mi socket = socket(af INET6, SOCK STREAM, O); 4.2. Estructura addrinfo En la sección Resolución de DNS vimos como netdb.h define funciones y estructuras para interactuar a modo de obtener la dirección de un host. Esta biblioteca además posee las capacidades que nos ayudarán a trabajar con las estructuras de dirección tanto de IPv4 como en IPv6 indistintamente. Tenemos: #include<netdb.h> struct addrinfo { int ai_flags; int ai_family; /* Familia del protocolo. */ int ai_socktype; /* Tipo de conexión. */ int ai_protocol; /* Protocolo de conexión. */ size_t ai_addrlen; /* Largo de la dirección. */ struct sockaddr *ai_addr; /* Estructura de dirección. */ char *ai_canonname; /* Nombre canonico. */ struct addrinfo *ai_next; /* Siguiente elemento de la lista. */ ; /* Para resolver cierta dirección tenemos: */ int getaddrinfo(char *direccion, char *servicio, struct addrinfo *consulta, struct addrinfo **respuesta); /* Para liberar la memoria */ void freeaddrinfo(struct addrinfo *respuesta); req.ai_flags = 0; req.ai_socktype = SOCK_STREAM; /* Tipo de conexion */ req.ai_protocol = 0; /* Protocolo de conexion. TCP por defecto */ Así, nuestro código quedará de la siguiente forma, mostrandose IPv4 e IPv6 al mismo tiempo: #include <netinet/in.h> /* Estructuras y constantes de dirección. */ #include <sys/socket.h> /* Funciones para el manejo de sockets. */ #include <netdb.h> /* Estructura y funciones para DNS. */ #define SERVER 0 7
#define CLIENT 1 /* == Parámetros == */ char direccion[len] = "www.ejemplo.com"; /* Puede ser una dirección ip. */ char puerto[6] = "4040"; /* Puerto al que nos conectaremos, ahora es string. * Acepta nombres de servicios descritas en: /etc/services */ int v = VERSION; /* VERSION será 4 para IPv4 y 6 para IPv6.*/ int c = TIPO_APP; /* TIPO_APP será SERVER o CLIENT. */ /* == Variables == */ struct addrinfo consulta, *respuesta; int mi_socket; if(v == 4) consulta.ai_family = PF_INET; /* Igual a AF_INET, para IPv4. */ else if(v == 6) consulta.ai_family = PF_INET6; /* Igual a AF_INET6, para IPv6. */ /* Opciones de ai_flags: http://manpages.ubuntu.com/manpages/maverick/es/man3/getaddrinfo.3.html */ consulta.ai_flags = AI_PASSIVE; /* Para poder hacer bind(). */ getaddrinfo(null, puerto, &consulta, &respuesta); /* Consulta al DNS. */ else if(c == CLIENT){ consulta.ai_flags = 0; getaddrinfo(direccion, puerto, &consulta, &respuesta); /* Consulta al DNS. */ /* Ahora en respuesta tenemos nuestra direccion configurada, luego trabajamos los * * sockets sin la necesidad de tener en cuenta la version de IP utilizada. */ my_socket = socket(respuesta->ai_family, respuesta->ai_socktype, respuesta->ai_protocol); bind(mi_socket, respuesta->ai_addr, respuesta->ai_addrlen); listen(mi_socket, BACKLOG); else if(c == CLIENT){ connect(my_socket, respuesta->ai_addr, respuesta->ai_addrle); Como vemos, la única diferencia entre los protocolos radica en la asignación de ai family, la cual puede ser hecha con un simple if. 8
5. Referencias [1] Nuevas funciones para sockets. http://uw714doc.sco.com/en/sdk\_netapi/sockc.portipv4appipv6.html [2] Trac del curso, Taller de redes 2014 https://scm.labit.inf.utfsm.cl/trac/tredes14/wiki/migrando\ %20aplicaciones\%20a\%20IPv6 [3] Como usar threads en C. http://timmurphy.org/2010/05/04/pthreads-in-c-a-minimal-working-example/ [4] Acerca de error en bind(). http://stackoverflow.com/questions/4160347/close-vs-shutdown-socket [5] Sobre como pasar argumentos. https://lihuen.info.unlp.edu.ar/index.php?title=parsear\_argumentos\_en\_forma\_elegante\ _con\_getopt\%28\%29 9