Angel está finalizando sus estudios de Ingeniería Informática. Actualmente trabaja como instructor en Sun Microsystems, impartiendo clases de administración de redes y sistemas Sun Solaris. Recientemente a publicado en la editorial Ra-Ma el libro titulado Protocolos de Internet. Diseño e Implementación en Sistemas UNIX. Sus principales áreas de interés son las redes, la seguridad, la programación de sistemas/redes Unix y últimamente el "Linux kernel hacking" le está quitando algunas horas de sueño ;)
El artículo pretende ser una introducción a la tecnología multicast en redes TCP/IP. Además de tratar los conceptos teóricos básicos de las comunicaciones multicast, se detalla el API de Linux que podemos usar como programadores para la realización de aplicaciones multicast. Se dan a conocer también las funciones del kernel que implementan esta tecnología, para obtener de esta manera una visión global del soporte multicast en Linux. El artículo finaliza con un sencillo ejemplo práctico de sockets en C, en el que se ilustra cómo realizar una aplicación multicast.
Los 4 bits de mayor peso de la dirección IP permiten direccionar
entre el valor 224 y el 239. Los 28 bits restantes de menor peso, están
reservados para el identificador del grupo multicast, tal y como se muestra
en el siguiente gráfico:
Las direcciones multicast IPv4 a nivel de red, deben mapearse sobre las direcciones físicas correspondientes al tipo de red con el se esté trabajando. Si se estuviese trabajando con direcciones a nivel de red unicast, se obtendría la dirección física asociada haciendo uso del protocolo ARP, en el caso de direcciones de red multicast, no se puede usar ARP y habrá que obtener la dirección física asociada mediante un procedimiento diferente. Se han definido varios documentos RFC (Request For Comments) que especifican la forma de realizar este mapeo:
Por ejemplo, la dirección multicast IPv4 224.0.0.5 se correspondería
con la dirección física Ethernet 01:00:5E:00:00:05.
Hay algunas direcciones multicast IPv4 especiales:
La siguiente tabla muestra el espacio de direccionamiento multicast
completo, junto con las denominaciones comunes para cada rango y el TTL
asociado a cada uno de ellos. El TTL en el multicast IPv4 tiene un doble
significado. Por un lado controla el tiempo de vida de un datagrama en
la red, como el lector ya sabrá, para evitar que un datagrama entre
en un bucle infinito, en caso de que exista una mala configuración
de las tablas de encaminamiento. Si además estamos trabajando con
multicast, el TTL define el ámbito del datagrama, es decir, cómo
de lejos llegará. De esta forma se puedan definir varios ámbitos
de alcance de los datagramas según la categoria a la que pertenezcan:
|
|
|
|
Nodo | 0 | El datagrama está restringido al propio host. No saldrá por ninguno de sus interfaces de red. | |
Enlace | 1 | 224.0.0.0 - 224.0.0.255 | El datagrama está restringido a la subred local al host que lo envía, no será encaminado por ningún router. |
Departamento | < 32 | 239.255.0.0 - 239.255.255.255 | Restringido a un departamento concreto dentro de la organización. |
Organización | < 64 | 239.192.0.0 - 239.195.255.255 | Restringido a una organización concreta. |
Global | < 255 | 224.0.1.0 - 238.255.255.255 | Sin restricción. Su ámbito es global. |
Si el host se ha unido a un grupo multicast, el interface de red deberá reconocer también como tramas destinadas a él, todas aquellas cuya dirección de destino sea la correspondiente al grupo de multicast al cual se haya unido el host.
Por tanto, si un host de una red tiene un interface cuya dirección física es 80:C0:F6:A0:4A:B1 y además se ha unido al grupo 224.0.1.10, las tramas que reconocerá como destinadas a él serán aquellas cuya dirección de destino sea alguna de las siguientes:
Los routers además envían de forma periódica mensajes IGMP al grupo 224.0.0.1 solicitando a los hosts información sobre los grupos a los cuales están asociados. Un host, al recibir este mensaje inicializa un temporizador con un valor aleatorio, y no contestará hasta que este temporizador llegue a cero. Con esto se evita que todos los hosts contesten a la vez, produciendo una sobrecarga innecesaria en la red. Cuando el temporizador de alguno de los hosts llegue a cero, enviará su contestación a la dirección del grupo multicast concreto del cual esté informando, por lo que el resto de hosts asociado a ese grupo verán la contestación, y anularán su temporizador no generando por tanto su respuesta. Esto se hace porque con un host que conteste es suficiente, al router únicamente le hace falta saber que hay un host interesado en determinado grupo en esa subred, con eso le basta para redirigir los mensajes multicast destinados al grupo, el resto de hosts los recibirán y no es necesario por tanto que también contesten ellos.
Si todos los hosts que estaban en un determinado grupo, se quitan del mismo, entonces ninguno contestará a los mensajes del router, quién al ver que ya no hay nadie interesado en determinado grupo en una subred, dejará de encaminar a la misma los mensajes destinados a ese grupo. Otra opción, implementada en IGMPv2, es que el propio host indique a los routers que ha abandonado un determinado grupo, enviando para ello un mensaje a la dirección 224.0.0.2.
Opción IPv4 | Tipo de Datos | Descripción |
IP_ADD_MEMBERSHIP | struct ip_mreq | Unirse a un grupo multicast. |
IP_DROP_MEMBERSHIP | struct ip_mreq | Abandonar un grupo multicast. |
IP_MULTICAST_IF | struct ip_mreq | Especificar un interface de red concreto para el envío de mensajes multicast. |
IP_MULTICAST_TTL | u_char | Especificar un TTL para el envío de mensajes multicast. |
IP_MULTICAST_LOOP | u_char | Activar o desactivar el loopback para los mensajes multicast. |
La estructura ip_mreq se define de la siguiente forma en el fichero de cabecera <linux/in.h>:
struct ip_mreq {
struct in_addr imr_multiaddr; /* IP multicast address of
group */
struct in_addr imr_interface; /* local IP address of interface
*/
};
Las opciones multicast se definen en este fichero de la siguiente forma:
#define IP_MULTICAST_IF 32
#define IP_MULTICAST_TTL 33
#define IP_MULTICAST_LOOP 34
#define IP_ADD_MEMBERSHIP 35
#define IP_DROP_MEMBERSHIP 36
El siguiente código corresponde a un servidor que envía al grupo multicast 224.0.1.1 todo aquello que recibe por la entrada estándard. Como se puede comprobar en el código, no hay que realizar ninguna acción especial para enviar información a un grupo multicast, con indicar como dirección de destino la del grupo es suficiente.
Se podrian haber cambiado las opciones de loopback y TTL si los valores que toman por defecto no fuesen adecuados para la aplicación que se esté desarrollando.
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#define MAXBUF 256
#define PUERTO 5000
#define GRUPO "224.0.1.1"
int main(void) {
int s;
struct sockaddr_in srv;
char buf[MAXBUF];
bzero(&srv, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_port = htons(PUERTO);
if (inet_aton(GRUPO, &srv.sin_addr) < 0) {
perror("inet_aton");
return 1;
}
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket");
return 1;
}
while (fgets(buf, MAXBUF, stdin)) {
if (sendto(s, buf, strlen(buf), 0, (struct sockaddr
*)&srv, sizeof(srv)) < 0) {
perror("recvfrom");
} else {
fprintf(stdout, "Enviado a %s: %s\n",
GRUPO, buf);
}
}
}
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#define MAXBUF 256
#define PUERTO 5000
#define GRUPO "224.0.1.1"
int main(void) {
int s, n, r;
struct sockaddr_in srv, cli;
struct ip_mreq mreq;
char buf[MAXBUF];
bzero(&srv, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_port = htons(PUERTO);
if (inet_aton(GRUPO, &srv.sin_addr) < 0) {
perror("inet_aton");
return 1;
}
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket");
return 1;
}
if (bind(s, (struct sockaddr *)&srv, sizeof(srv)) <
0) {
perror("bind");
return 1;
}
if (inet_aton(GRUPO, &mreq.imr_multiaddr) < 0) {
perror("inet_aton");
return 1;
}
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq,
sizeof(mreq)) < 0) {
perror("setsockopt");
return 1;
}
n = sizeof(cli);
while (1) {
if ((r = recvfrom(s, buf, MAXBUF, 0, (struct
sockaddr *)&cli, &n)) < 0) {
perror("recvfrom");
} else {
buf[r] = 0;
fprintf(stdout, "Mensaje desde %s:
%s\n", inet_ntoa(cli.sin_addr), buf);
}
}
}
struct ip_mreqn mreq;
if (optlen < sizeof(struct ip_mreq))
return -EINVAL;
if (optlen >= sizeof(struct ip_mreqn)) {
if(copy_from_user(&mreq,optval,sizeof(mreq)))
return -EFAULT;
} else {
memset(&mreq, 0, sizeof(mreq));
if (copy_from_user(&mreq,optval,sizeof(struct ip_mreq)))
return -EFAULT;
}
if (optname == IP_ADD_MEMBERSHIP)
return ip_mc_join_group(sk,&mreq);
else
return ip_mc_leave_group(sk,&mreq);
Las primeras líneas de código comprueban que el parámetro de entrada, la estructura ip_mreq, tiene una longitud adecuada y se puede obtener copiar correctamente desde la zona de usuario a la zona del kernel. Una vez obtenido el parámetro, se invoca a ip_mc_join_group() para unirse a un grupo nulticast, o a ip_mc_leave_group() para abandonarlo.
El código de estas funciones se puede encontrar en /usr/src/linux/net/ipv4/igmp.c.
El código para unirse a un grupo es el siguiente:
int ip_mc_join_group(struct sock *sk , struct ip_mreqn *imr)
{
int err;
u32 addr = imr->imr_multiaddr.s_addr;
struct ip_mc_socklist
*iml, *i;
struct in_device *in_dev;
int count = 0;
Lo primero es comprobar, mediante la macro MULTICAST, si la dirección del grupo es correcta deacuerdo a los rangos definidos para las direcciones de este tipo. Simplemente se comprueba que el byte de mayor peso de la dirección IP tiene un valor de 224.
if (!MULTICAST(addr))
return -EINVAL;
rtnl_shlock();
A continuación se establece el interface de red al cual se asociará el grupo multicast indicado. Si no se puede acceder al interface por índice, como es habitual en IPv6, se llama a la función ip_mc_find_dev() que encuentra el dispositivo asociado a una dirección IP concreta. Este camino será el que se tome para el ejemplo del artículo, ya que se trabaja con IPv4. Si como dirección se indicó INADDR_ANY, el kernel deberá encontrar por sí mismo el interface de red adecuado, para ello mirará en la tabla de rutas para ver cuál es el interface adecuado teniendo en cuenta la dirección del grupo y las rutas actuales establecidas.
if (!imr->imr_ifindex)
in_dev = ip_mc_find_dev(imr);
else
in_dev = inetdev_by_index(imr->imr_ifindex);
if (!in_dev) {
iml = NULL;
err = -ENODEV;
goto done;
}
El código siguiente reserva memoria para una estructura de tipo ip_mc_socklist. Y compara la dirección de cada grupo e interface asociado al socket, con los datos de entrada a la función. Si alguna de las entradas asociadas al socket con anterioridad coincide, salimos directamente ya que no tiene sentido asociarse dos veces a la mismo grupo e interface. Si no se indicó INADDR_ANY como dirección para el interface de red, entonces se aumenta el contador de referencias a esta entrada antes de salir de la función.
iml = (struct ip_mc_socklist
*)sock_kmalloc(sk, sizeof(*iml),
GFP_KERNEL);
err = -EADDRINUSE;
for (i=sk->ip_mc_list;
i; i=i->next) {
if (memcmp(&i->multi, imr, sizeof(*imr)) == 0) {
/* New style additions are reference counted */
if (imr->imr_address.s_addr == 0) {
i->count++;
err = 0;
}
goto done;
}
count++;
}
err = -ENOBUFS;
if (iml == NULL || count
>= sysctl_igmp_max_memberships)
goto done;
Si se llega a este punto, significa que se quiere enlazar un socket a un grupo nuevo, por lo que hay que crear una nueva entrada y enlazar la al comienzo de la lista de grupos perteneciente al socket. La memoria se reservó anteriormente, únicamente queda establecer los valores correctos a los campos de las estructuras de datos involucradas.
memcpy(&iml->multi,
imr, sizeof(*imr));
iml->next = sk->ip_mc_list;
iml->count = 1;
sk->ip_mc_list = iml;
ip_mc_inc_group(in_dev,
addr);
iml = NULL;
err = 0;
done:
rtnl_shunlock();
if (iml)
sock_kfree_s(sk, iml, sizeof(*iml));
return err;
}
La función ip_mc_leave_group() usada para abandonar un grupo multicast, es más simple que la función anterior. Recibida la dirección del interface y el grupo, se buscan estos datos entre las entradas asociadas al socket que se esté manejando. Una vez encontrada la entrada, se decrementa el número de referencias, ya que hay un proceso menos asociado al grupo. Si al decrementar el número de referencias, toma el valor cero, la entrada se elimina.
int ip_mc_leave_group(struct sock *sk, struct ip_mreqn *imr)
{
struct ip_mc_socklist
*iml, **imlp;
for (imlp=&sk->ip_mc_list;
(iml=*imlp)!=NULL; imlp=&iml->next) {
if (iml->multi.imr_multiaddr.s_addr==imr->imr_multiaddr.s_addr
&& iml->multi.imr_address.s_addr==imr->imr_address.s_addr
&&
(!imr->imr_ifindex ||
iml->multi.imr_ifindex==imr->imr_ifindex)) {
struct in_device *in_dev;
if (--iml->count)
return 0;
*imlp = iml->next;
synchronize_bh();
in_dev = inetdev_by_index(iml->multi.imr_ifindex);
if (in_dev)
ip_mc_dec_group(in_dev,
imr->imr_multiaddr.s_addr);
sock_kfree_s(sk, iml, sizeof(*iml));
return 0;
}
}
return -EADDRNOTAVAIL;
}
El resto de opciones multicast vistas son muy simples, ya que se limitan
a establecer directamente ciertos valores en campos de datos de la estructura
interna asociada al socket que estemos manejando. Estas asignaciones se
realizan directamente en la función ip_setsockopt().