Multicast

ArticleCategory: []

System Administration

AuthorImage:[]

[Photo of the Author]

TranslationInfo:[]

original in es Angel Lopez

AboutTheAuthor:[]

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 ;)

Abstract:[]

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.

ArticleIllustration:[]

[Ilustration]

ArticleBody:[]

Introducción

A la hora de direccionar un host (interface) dentro de una red, se puede hacer uso de tres tipos diferentes de direcciones: Se usarán direcciones multicast cuando el destinatario de la información no sea una única maquina, pero tampoco se quiera hacer un broadcast a toda la red. Este escenario será típico de situaciones en las que se requiera el envío de información mutimedia (audio o video en tiempo real) a varios hosts de la red. En casos como este no es óptimo, en términos de ancho de banda, establecer un envío unicast a cada uno de los clientes que quieran recibir la emisión multimedia. Establecer un envío broadcast tampoco es la solución, sobretodo si alguno de los clientes están fuera de la subred local desde la cual se realiza el envío.

Direcciones Multicast

Como el lector ya sabrá, el espacio de direccionamiento IP se distribuye en tres grupos o clases de direcciones, las direcciones de clase A, B y C. Hay una cuarta clase, la clase D, reservada para las direcciones multicast. La clase D tiene reservado el rango de direcciones IPv4 entre la 224.0.0.0 y la 239.255.255.255.

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:

En las redes Ethernet, que son las redes más comunes, el mapeo se realiza colocando en los 24 bits de mayor peso de la dirección Ethernet los valores 01:00:5E. El siguiente bit siempre tiene un valor de 0 y los 23 bits de menor peso restantes contienen el valor de los 23 bits de menor peso de la dirección multicast IPv4. Este proceso se muestra en el siguiente gráfico:

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:

Hay más direcciones multicast reservadas que las aquí mostradas, para una referencia completa consultar la última versión disponible del RFC "Assigned Numbers".

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:
 

Ambito
TTL
Rango de Direcciones
Descripción
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.

Funcionamiento del Multicast

En una LAN, un interface de red de un host subirá a niveles superiores todas aquellas tramas que considere que van destinadas a él. Estas tramas serán aquellas que tengan como dirección de destino la dirección física asociada al interface, o aquellas tramas cuya dirección de destino sea la dirección de broadcast.

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:

En el caso de querer trabajar con multicast en WAN, se necesitan routers con soporte multicast que se comuniquen entre ellos mediante algún protocolo de encaminamiento que contemple el multicast. Cuando un proceso en un host de una subred se asocia a un grupo multicast, este host envia un mensaje IGMP a todos los routers multicast de su subred, informándoles que cuando reciban un mensaje multicast destinado al grupo al cual él se ha asociado, lo envíen a la subred para que pueda recibirlo. Estos routers le comunicarán esta información al resto de routers multicast de tal forma que todos los routers sepan a quién deberán encaminar los mensajes multicast que le lleguen.

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.

API de programación

Si el lector tiene experiencia previa en la programación de sockets, únicamente le será necesario conocer cinco opciones nuevas de los sockets para el manejo de las opciones multicast. Se usarán las funciones setsockopt() y getsockopt() para establecer o leer el valor de estas opciones. La tabla siguiente muestra las opciones disponibles para multicast, junto con el tipo de datos que maneja y su descripción:
 
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

IP_ADD_MEMBERSHIP

Un proceso puede unirse a un grupo multicast usando esta opción sobre un socket con la función setsockopt(). Como parámetro se indica una estructura de tipo ip_mreq. En el primer campo de la estructura, imr_multiaddr, se indica la dirección multicast del grupo al cual queremos unirnos. En el segundo campo, imr_interface, se indica la dirección IPv4 del interface de red que queremos usar.

IP_DROP_MEMBERSHIP

Mediante esta opción, un proceso puede abandonar un grupo multicast. Los campos de la estructura ip_mreq se usan de igual forma que en el caso anterior.

IP_MULTICAST_IF

Esta opción permite establecer el interface de red que será usado para enviar los mensajes multicast que sean escritos en el socket. El interface se indicará en la estructura ip_mreq como en las opciones anteriores.

IP_MULTICAST_TTL

Establece el TTL (Time To Live) de los datagramas que contendrán los mensajes multicast que sean enviados por el socket. Por defecto, el TTL asignado es de 1, es decir, el datagrama no saldrá de la subred local.

IP_MULTICAST_LOOP

Cuando un proceso envía un mensaje a un grupo multicast, si el interface de salida del mensaje pertenece al grupo, el mensaje será recibido por el propio proceso emisor como si hubiese llegado por la red. Con esta opción se puede activar o desactivar este comportamiento.

Ejemplo práctico

Para probar las ideas que se han comentado en este artículo, se muestra a continuación un ejemplo simple, en el cual tenemos un proceso que envía mensajes a un grupo multicast concreto, y varios procesos que se asocian a ese grupo y reciben los mensajes, mostrándolos por pantalla.

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.

Servidor

La información recibida por su entrada estándar la envía al grupo de multicast 224.0.1.1

#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);
    }
  }
}
 

Cliente

El código que se muestra a continuación corresponde al cliente, el cual recibe la información que el servidor envía al grupo multicast. Los mensajes recibidos los muestra por su salida estándard. La única particularidad de este código, es el establecimiento de la opción IP_ADD_MEMBERSHIP. El resto del código es el estándar para un proceso que desee recibir mensajes UDP.

#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);
    }
  }
}

Multicast y el Kernel

Como se acaba de ver, cuando un proceso se quiere unir a un grupo, usa la función setsockopt() para establecer en el nivel de IP la opción IP_ADD_MEMBERSHIP. La implementación de esta función se puede encontrar en /usr/src/linux/net/ipv4/ip_sockglue.c. El código que se ejecuta dentro de esta función, para esta opción y para la opción IP_DROP_MEMBERSHIP es el siguiente:

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().