Next Previous Contents

7. Por dentro.

El objetivo de esta sección es el de dar alguna información, que no es necesaria para un entendimiento básico de cómo funciona multicast ni para escribir programas multicast, pero que sí es muy interesante, indicando como funcionan los protocolos e implementaciones de multicast que hay por debajo, y que puede ser útil para evitar errores y malentendidos comunes.

7.1 IGMP.

Cuando hablamos de IP_ADD_MEMBERSHIP y IP_DROP_MEMBERSHIP, dijimos que la información ofrecida por estos «comandos» era usada por el kernel para elegir qué datagramas aceptar o descartar. Esto es cierto, pero no es toda la verdad. Una simplificación así indicaría que los datagramas para todos los grupos multicast en el mundo serían recibidos por nuestro ordenador, el cual comprobaría en función de lo indicado por los procesos que se estuvieran ejecutando, si pasar el tráfico a estos o descartarlo. Como puedes imaginar, esto sería un completo desaprovechamiento de ancho de banda.

Lo que pasa realmente es que los ordenadores indican a sus encaminadores indicándoles cuales son los grupos multicast en los que están interesados; entonces, todos los encaminadores indican a su vez a los encaminadores de mayor nivel que desean recibir este tráfico, y así continuamente. Los algoritmos empleados para tomar la decisión de cuándo pedir el tráfico de un grupo o decir que no se desea más, varían mucho. Hay algo que nunca cambia: cómo se transmite esta información. Se utiliza IGMP para esto. Significa Protocolo de Gestión de Grupos de Internet (Internet Group Management Protocol). Se trata de un nuevo protocolo, parecido en muchos aspectos a ICMP, con un número de protocolo de 2, cuyos mensajes se envían en datagramas IP, y que debe estar implementado en todos los ordenadores de nivel 2.

Como se ha dicho antes, se utiliza tanto para que los ordenadores den información a los encaminadores, como para que los encaminadores hablen entre sí. De aquí en adelante sólo cubriré las relaciones ordenadores-encaminadores, principalmente porque no he sido capaz de encontrar información describiendo la comunicación encaminador a encaminador aparte del código fuente de mrouted (el RFC 1075 que describe el Protocolo de Encaminamiento Multicast basado en vector de distancias -Distance Vector Multicast Routing Protocol, DVMRP- está ya obsoleto, y mrouted implementa un DVMRP que no ha sido aún documentado).

El RFC 988 especifica IGMP versión 0, que ahora está obsoleto. Casi nadie utiliza la versión 0.

El RFC 1112 describe la versión 1 de IGMP y, aunque se ha actualizado con el RFC 2236 (IGMP versión 2), aún se utiliza ampliamente. El kernel Linux (2.0.33) implementa la versión 1 por completo y parte de los requisitos de la versión 2, pero no todos.

Intentaré ahora dar una descripción informal del protocolo. Puede comprobar el RFC 2236 para una descripción formal, con gran número de diagramas de estado y fronteras de «timeout».

Todos los mensajes IGMP tienen la siguiente estructura:

    0 1 2 3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   | Tipo |Max Tiempo Resp| Checksum |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   | Dirección de Grupo |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

La versión 1 de IGMP (de aquí en adelante IGMPv1) marca el campo «Max Tiempo Resp» como «No usado», lo pone a cero cuando lo envía, y lo ignora cuando lo recibe. Asimismo parte el campo «Tipo» en dos campos de 4 bits: «Versión» y «Tipo». Dado que IGMPv1 marca una «pregunta de miembros» como mensaje 0x11 (versión 1, tipo 1) y IGMPv2 también lo hace como 0x11, los 8 bits tienen la misma interpretación en ambos.

Creo que es más instructivo dar primero la descripción de IGMPv1 y después destacar lo que añade IGMPv2, ya que son fundamentalmente eso, cosas adicionales.

Para lo siguiente es importante recordar que los encaminadores de multicast reciben todos los datagramas de multicast IP.

IGMP versión 1.

Los encaminadores envían periódicamente Preguntas IGMP de Pertenencia de Ordenadores (IGMP Ordenador Membership Queries, n. del t.) al grupo todos-los-ordenadores (224.0.0.1) con un TTL de 1 (cada minuto más o menos). Todos los ordenadores con capacidades multicast lo «escuchan», pero no responden inmediatamente para evitar una tormenta de respuestas. En lugar de esto, ponen un temporizador con un valor aleatorio para cada grupo al que pertenecen en el interfaz donde recibieron la pregunta.

Más pronto o más tarde, el temporizador llega a cero en uno de los ordenadores, y éste envía un paquete IGMP Respuesta de Pertenencia (Ordenador Membership Report, n. del t.) (también con TTL 1) a la dirección multicast del grupo sobre el que responde. Ya que se envía al grupo, todos los ordenadores que se unieron a ese grupo (y que están esperando que su propio temporizador se ponga a cero) también lo reciben. Entonces, paran sus temporizadores y no generan otra respuesta. Solo una es generada (por el ordenador que obtiene el menor valor aleatorio), y esto es suficiente para el encaminador. Sólo necesita saber que hay miembros para este grupo en la subred, no quienes ni cuántos.

Cuando no se recibe ninguna respuesta para un grupo determinado después de algún número de preguntas, el encaminador asume que no queda ningún miembro, y por tanto no tiene que encaminar el tráfico para ese grupo en esa subred. Obsérvese que en IGMPv1 no hay ningún mensaje de «Abandono de Grupo».

Cuando un ordenador se une a un nuevo grupo, el kernel envía un aviso para ese grupo, de forma que los procesos respectivos no tienen que esperar uno o dos minutos hasta que se reciba una pregunta con respecto a ese grupo. Este paquete IGMP se genera por el kernel como respuesta al comando IP_ADD_MEMBERSHIP,visto en la sección IP_ADD_MEMBERSHIP.

Nótese el énfasis en el adjetivo «nuevo»: si un proceso solicita un IP_ADD_MEMBERSHIP para un grupo del cual el ordenadores ya es miembro, no se envía ningún paquete IGMP dado que ya se debe estar recibiendo tráfico para este grupo; en su lugar, se incrementa un contador de miembros para ese grupo. IP_DROP_MEMBERSHIP no genera ningún datagrama en IGMPv1.

Las preguntas de pertenencia de ordenadores se identifican con el Tipo 0x11, y las respuestas con el Tipo 0x12.

No se envían nunca respuestas para el grupo todo-ordenadores. La pertenencia a este grupo es permanente.

IGMP versión 2.

Una adición importante a lo anterior es la inclusión de un mensaje Abandonar Grupo (Tipo 0x17). La razón para esta inclusión es la de reducir el gasto de ancho de banda entre el tiempo que el último ordenador de una subred abandona un grupo y el tiempo que el encaminador abandona todas sus preguntas y decide que no hay más miembros presentes para ese grupo. Los mensajes de Abandonar Grupo deben dirigirse al grupo todo-encaminadores (224.0.0.2) en lugar de al grupo que se deja, dado que esta información no tiene ninguna utilidad a otro miembros (las versiones del kernel hasta la 2.0.33 los envían a este grupo; aunque no hace ningún daño a los otros ordenadores, es un gasto de tiempo ya que tienen que procesarlo, y sin embargo no obtienen información útil). Hay ciertos detalles sutiles respecto a cuándo o cuándo no enviar estos mensajes; si le interesa, consulte el RFC.

Cuando un encaminador IGMPv2 recibe un mensaje de abandono para un grupo, envía Preguntas específicas de Grupo (Group-Specific Queries , n. del t.) al grupo que se deja. Esto es otra novedad. IGMPv1 no tiene ningún tipo de preguntas específicas para grupos. Todas las preguntas se envían al grupo todo-ordenadores. El Tipo en la cabecera IGMP no varía (0x11 como antes), pero la «Dirección de grupo» se rellena con la del grupo multicast que se abandona.

El campo «Max Tiempo Resp», que se ponía a 0 en transmisión y que se ignoraba en recepción en IGMPv1, es significativo sólo en mensajes del tipo «Pregunta de pertenencia». Se da el máximo tiempo permitido antes de enviar una respuesta, en unidades de 1/10 segundo. Se utiliza como mecanismo de ajuste.

IGMPv2 añade otro tipo de mensaje: 0x16. Es una «Respuesta de Pertenencia Versión 2» enviada por los ordenadores IGMPv2 si detectan un encaminador IGMPv2 (un ordenador IGMPv2 sabe que hay un encaminador IGMPv1 cuando recibe una pregunta con el campo «Max Respuesta» a 0).

Cuando más de un encaminador desea actuar como entrevistador, IGMPv2 provee un mecanismo para evitar «discusiones»: el encaminador con la dirección IP más baja se elige como entrevistador. Los otro encaminadores activan un temporizador. Si el encaminador con la dirección IP más baja se cae o se apaga, la decisión de quién va a ser el entrevistador se vuelve a tomar cuando los temporizadores expiran.

7.2 El rincón del Kernel.

Esta subsección da algunos puntos de entrada para estudiar la implementación de multicast en el kernel de Linux. No explica esta implementación. Sólo dice dónde encontrar las cosas.

El estudio fue realizado sobre la versión 2.0.32, así que puede haber cambiado para cuando leas esto (el código fuente de red parece haber cambiado MUCHO en las versiones 2.1.x, por ejemplo).

El código fuente multicast en el kernel de Linux siempre está rodeado por pares #ifdef CONFIG_IP_MULTICAST / #endif , para que pueda incluir/excluirlo de su kernel según sus intereses (esta inclusión/exclusión se realiza en el momento de compilar, como probablemente sepa si lee esta sección...). Los #ifdefs son manejados por el preprocesador. La decisión se realiza basándose en lo que elegió cuando hizo, bien make config, make menuconfig o make xconfig).

Quizás quiera las capacidades multicast, pero si su ordenador Linux no va a trabajar como encaminador posiblemente no quiera las funciones incluidas en su nuevo kernel. Por esto el código de encaminamiento multicast esta rodeado por pares #ifdef CONFIG_IP_MROUTE / #endif.

El código fuente del kernel generalmente reside en /usr/src/linux. Sin embargo, el lugar puede variar, así que para mayor exactitud y brevedad, me referiré al directorio raíz del código del kernel como simplemente LINUX. Así, algo como LINUX/net/ipv4/udp.c es equivalente a /usr/src/linux/net/ipv4/udp.c si desempaquetó el código fuente en el directorio /usr/src/linux.

Todas las comunicaciones multicast entre los programas de usuario mostrados en la sección dedica a la programación multicast y el kernel se realizaron a través de las llamadas al sistema setsockopt()/getsockopt(). Ambas son implementadas a través de funciones que hacen algunas pruebas para verificar los parámetros que se le entregan y las cuales, a su vez, llaman a otras funciones que hacen comprobaciones adicionales, demultiplexan la llamada basándose en el parámetro nivel a otra llamada al sistema, y después llaman a otra función que ... (si le interesan todos estos saltos, puede seguirlos en LINUX/net/socket.c -funciones sys_socketcall() y sys_setsockopt() -, LINUX/net/ipv4/af_inet.c -función inet_setsockopt()- y LINUX/net/ipv4/ip_sockglue.c -función ip_setsockopt() ).

El fichero que nos interesa es LINUX/net/ipv4/ip_sockglue.c, donde encontraremos ip_setsockopt() y ip_getsockopt() que son básicamente un switch (después de alguna comprobación de errores) que verifican todos los posibles valores para optname. Aquí se manejan las opciones unicast, y también las multicast: IP_MULTICAST_TTL, IP_MULTICAST_LOOP, IP_MULTICAST_IF, IP_ADD_MEMBERSHIP y IP_DROP_MEMBERSHIP. Antes del switch, se realiza una comprobación para determinar si las opciones son específicas de un encaminador de multicast, y, de ser así, son reenviada a las funciones ip_mroute_setsockopt() y ip_mroute_getsockopt() (fichero LINUX/net/ipv4/ipmr.c).

En LINUX/net/ipv4/af_inet.c podemos ver los valores por defecto de los que hablamos en secciones anteriores (loopback habilitado, TTL=1) dadas cuando el socket se crea (obtenido de la función inet_create() en este fichero):

 
        #ifdef CONFIG_IP_MULTICAST
        sk->ip_mc_loop=1;
        sk->ip_mc_ttl=1;
        *sk->ip_mc_name=0;
        sk->ip_mc_list=NULL; #endif

Asimismo la aseveración de «cerrar el socket hace que el kernel abandone todas las asociaciones que este socket tenía» se corroboran por:

#ifdef CONFIG_IP_MULTICAST
                /* Las aplicaciones se olvidan de abandonar los grupos
                antes de salir */
                ip_mc_drop_socket(sk); #endif

Obtenido de inet_release(), en el mismo fichero que el anterior.

Las operaciones independientes de dispositivo para la capa de Enlace se guardan en LINUX/net/core/dev_mcast.c.

Dos funciones importantes siguen faltando: el proceso de la entrada y salida de datagramas multicast. Como los demás datagramas, los datagramas entrantes son pasados de los controladores de dispositivos a la función ip_rcv() (LINUX/net/ipv4/ip_input.c). En esta función es donde se aplica el filtrado perfecto a los paquetes multicast que han pasado la capa de dispositivo (observe que las capas inferiores sólo hacen un filtro de mayor esfuerzo y es IP quien sabe al 100% si estamos interesados o no en un grupo multicast). Si el ordenador está actuando como encaminador multicast, esta función decide también si los datagramas deberían ser reenviados y llama a la función ipmr_forward() adecuadamente. (ipmr_forward() está implementada en LINUX/net/ipv4/ipmr.c).

El código que se encarga de los paquetes salientes se guarda en LINUX/net/ipv4/ip_output.c. Aquí es donde la opción IP_MULTICAST_LOOP entra en juego, ya que se comprueba para ver si los paquetes deben ser enviados a uno mismo o no (función ip_queue_xmit()). Asimismo el TTL de los paquetes salientes se elige en base a si es multicast o unicast. En este último caso, se utiliza el argumento dado a la opción IP_MULTICAST_TTL (función (ip_build_xmit()).

Mientras trabajábamos con mrouted (un programa que le da al kernel información de como encaminar los datagramas multicast), detectamos que todos los paquetes multicast originados en la red local eran encaminados adecuadamente... ¡¡excepto aquellos del propio ordenador GNU/Linux que estaba actuando como encaminador multicast!! ip_input.c estaba funcionando bien, pero parecía que ip_output.c no. Leyendo el código fuente de las funciones de salida, encontramos que los datagramas de salida no eran pasados a impr_forward(), la función que debía decidir si eran encaminados o no. Los paquetes salían a la red local pero, como generalmente las tarjetas de red son incapaces de leer sus propias transmisiones, estos datagramas nunca se encaminaban. Añadimos el código necesario a la función ip_build_xmit() y todo funcionó bien (tener las fuentes de su kernel no es un lujo ni una pedantería; es una necesidad!)

Se ha mencionado varias veces ipmr_forward(). Se trata de una función importante porque resuelve un malentendido que parece haberse extendido mucho. Cuando se encamina el tráfico multicast, no es mrouted quien hace copias y las manda a los receptores adecuados. mrouted recibe todo el tráfico multicast y, en función de esta información, realiza las tablas de encaminamiento multicast y le dice al kernel cómo encaminar: «datagramas para este grupo viniendo de este interfaz deben ser reenviados por estos interfaces». Esta información es enviada al kernel a través de llamadas a setsockopt() en un socket «raw» abierto por el demonio mrouted (el protocolo especificado cuando se creó el socket debe ser IPPROTO_IGMP). Estas opciones son manejadas por la función ip_mroute_setsockopt() de LINUX/net/ipv4/ipmr.c. La primera opción (sería más apropiado llamarlos comandos en vez de opciones) dada en este socket debe ser MRT_INIT. Todos los demás comandos son ignorados (devolviendo -EACCES) si no se envía primero MRT_INIT. Sólo puede estar ejecutándose una instancia de mrouted en el mismo ordenador. Para conseguir esto, cuando se envía el primer MRT_INIT, una importante variable, struct sock* mroute_socket, apunta al socket donde se recibió MRT_INIT. Si mroute_socket no es NULL cuando atiende un MRT_INIT significa que hay otro mrouted funcionando y se devuelve un -EADRRINUSE. Todos los comandos restantes (MRT_DONE, MRT_ADD_VIF, MRT_DEL_VIF, MRT_ADD_MFC, MRT_DEL_MFC y MRT_ASSERT) devuelven -EACCES si vienen de un socket distinto al mroute_socket.

Dado que los datagramas multicast encaminados pueden ser recibidos/enviados tanto a través de interfaces físicos como de túneles, se definió una abstracción común para ambos: IFV's, InterFaces Virtuales [Virtual InterFaces, n. del t.]. mrouted pasa estructuras ifv al kernel, indicando interfaces físicos o túneles que deben ser añadidos a sus tablas de encaminamiento, y entradas en el reenvío multicast diciendo dónde deben ser reenviados.

Los IVFs son añadidos con MRT_ADD_VIF y borrados con MRT_DEL_VIF. Ambos pasan un struct vifctl al kernel (definido en /usr/include/linux/mroute.h) con la siguiente información:

struct vifctl {
        vifi_t vifc_vifi; /* Indice de IFV */
        unsigned char vifc_flags; /* VIFF_ flags */
        unsigned char vifc_threshold; /* Limite ttl */
        unsigned int vifc_rate_limit; /* Rate limiter values (NI) */
        struct in_addr vifc_lcl_addr; /* Nuestra direccion */
        struct in_addr vifc_rmt_addr; /* direccion del tunel IPIP */
};

Con esta información se construye una estructura vif_device:

struct vif_device {
        struct device *dev; /* Dispositivo que estamos usando */
        struct route *rt_cache; /* Tunnel route cache */
        unsigned long bytes_in,bytes_out;
        unsigned long pkt_in,pkt_out; /* Estadisticas */
        unsigned long rate_limit; /* Traffic shaping (NI) */
        unsigned char threshold; /* Barrera TTL */
        unsigned short flags; /* Flags de Control */
        unsigned long local,remote; /* Direcciones(remotas para
tuneles)*/ };

Observe el campo dev en la estructura. La estructura device está definida en el fichero /usr/include/linux/netdevice.h. Es una estructura grande pero el campo que nos interesa es:

  struct ip_mc_list* ip_mc_list; /* cadena de filtro IP multicast */

La estructura ip_mc_list - definida en /usr/include/linux/igmp.h - es como sigue:

struct ip_mc_list {
        struct device *interface;
        unsigned long multiaddr;
        struct ip_mc_list *next;
        struct timer_list timer;
        short tm_running;
        short reporter;
        int users; };

Por tanto, el miembro ip_mc_list de la estructura dev es un puntero a una lista enlazada de estructuras ip_mc_list, y cada una contiene una entrada para cada grupo multicast del cual es miembro el interfaz de red. Una vez más vemos aquí que los miembros de un grupo son los interfaces. LINUX/net/ipv4/ip_input.c recorre esta lista enlazada para decidir si el datagrama recibido está destinados a algún grupo al que pertenezca el interfaz que recibió el datagrama.

#ifdef CONFIG_IP_MULTICAST
                if(!(dev->flags&IFF_ALLMULTI) &&
brd==IS_MULTICAST
                   && iph->daddr!=IGMP_ALL_ORDENADORES
                   && !(dev->flags&IFF_LOOPBACK))
                {
                        /*
                         * Comprueba que es para uno de nuestros
grupos
                         */
                        struct ip_mc_list *ip_mc=dev->ip_mc_list;
                        do
                        {
                                if(ip_mc==NULL)
                                {
                                        kfree_skb(skb, FREE_WRITE);
                                        return 0;
                                }
                                if(ip_mc->multiaddr==iph->daddr)
                                        break;
                                ip_mc=ip_mc->next;
                        }
                        while(1);
                } #endif

El campo users en la estructura ip_mc_list se utiliza para implementar lo indicado en la sección IGMP versión 1: si un proceso se une a un grupo y el interfaz ya es miembro de este grupo (esto es, otro proceso se unió a este mismo grupo en el mismo interfaz anteriormente) sólo se incrementa el contador (users). No se envían mensajes IGMP, como puede ver en el siguiente código (sacado de ip_mc_inc_group(), llamado por ip_mc_join_group(), ambos están en LINUX/net/ipv4/igmp.c):

        for(i=dev->ip_mc_list;i!=NULL;i=i->next)
        {
                if(i->multiaddr==addr)
                {
                        i->users++;
                        return;
                }
        }

Cuando se abandona un grupo, el contador se decrementa y se hacen operaciones adicionales sólo si la cuenta ha llegado a 0 (ip_mc_dec_group()).

MRT_ADD_MFC y MRT_DEL_MFC añaden o borran entradas de encaminamiento en las tablas de encaminamiento multicast. Ambas pasan una estructura struct mfcctl al kernel (también definida en /usr/include/linux/mroute.h) con esta información:

struct mfcctl {
        struct in_addr mfcc_origin; /* Origen de mcast */
        struct in_addr mfcc_mcastgrp; /* Grupo en cuestión */
        vifi_t mfcc_parent; /* Donde llegó */
        unsigned char mfcc_ttls[MAXVIFS]; /* A donde se dirige */ };

Con esta información en la mano, ipmr_forward() «recorre» los IVFs, y si se encuentra uno que case él duplica el datagrama y llama a ipmr_queue_xmit() el cual, a su vez, utiliza el dispositivo de salida especificado por la tabla de encaminamiento y el destino apropiado si el paquete debe ser enviado por un túnel (esto es, la dirección de destino unicast del otro extremo del túnel).

La función ip_rt_event() (no directamente relacionada con el envío, pero que también está en ip_output.c) recibe los eventos relacionados con un dispositivo de red, como que un dispositivo se está activando. Esta función asegura que el dispositivo se une al grupo multicast TODO-ORDENADORES.

Las funciones IGMP se implementan en LINUX/net/ipv4/igmp.c. Hay importante información para estas funciones en /usr/include/linux/igmp.h y /usr/include/linux/mroute.h.

La entrada IGMP en el directorio /proc/net se crea con ip_init() en LINUX/net/ipv4/ip_output.c.


Next Previous Contents