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.
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.
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.
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.
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 #ifdef
s 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
.