Linux IGMP SNOOPING 学习笔记 之二 igmp数据包处理

上一节分析了igmp snooping初始相关的流程,这一节主要是分析igmp 数据包处理流程。

一、br_multicast_rcv

该函数是igmp snooping留给网桥子系统的外部接口函数,当网桥接收了igmp数据包后就会调用该函数进行后续处理。

什么时候会调用到函数br_multicast_rcv?

主要有2个地方会调用到这个函数

a)网桥数据转发流程

当网桥端口接收到接收到数据包后,经过一系列处理后,在br_handle_frame_finish里,如果判断数据包的目的mac地址为0x01开头后,即认为是组播数据包,此时则会调用br_multicast_rcv进行igmp snooping相关的处理。

b)网桥接口br0的数据发送

当数据需要从网桥端口br发送出去时,则会调用网桥设备的发送函数br_dev_xmit,在br_dev_xmit里,如果判断数据包的目的mac地址为0x01开头后,同样会调用函数br_multicast_rcv进行igmp snooping相关的处理。

1、 br_multicast_rcv

/*

功能:处理igmp数据包

*/

int br_multicast_rcv(struct net_bridge *br,struct net_bridge_port *port,

struct sk_buff *skb)

{

BR_INPUT_SKB_CB(skb)->igmp= 0;

BR_INPUT_SKB_CB(skb)->mrouters_only= 0;

if(br->multicast_disabled)

return0;

switch(skb->protocol) {

casehtons(ETH_P_IP):

returnbr_multicast_ipv4_rcv(br, port, skb);

}

return0;

}

由于该函数有对skbuff的cb[]数据段的引用,下面介绍一下igmp snooping下cb的定义:

struct br_input_skb_cb {

structnet_device *brdev;

#ifdef CONFIG_BRIDGE_IGMP_SNOOPING

intigmp;

intmrouters_only;

#endif

};

对于支持igmpsnooping时,增加了两个参数:igmp、mrouters_only,

igmp:igmp表示数据包是否是igmp类型的报文,因为当目的mac地址是0x01开头时,既可

以是igmp类型的组播协议控制报文,也可以是组播数据流报文。

mrouters_only:在代码中只要igmp 加入报文才会将该值设置为1。在从网桥端口接收了一

个igmp 加入报文后,有如下走向:

a) 若br->router_list.first不为空,则会将这个igmp加入报文从该链表上的所有端口发送出去。

b) 将该igmp加入报文发送给上层协议栈进行处理。

在上面的函数中,首先将这两个值设置为0,然后对于三层协议为ip协议的数据,则会调用

br_multicast_ipv4_rcv进行igmp协议的处理。

二、br_multicast_ipv4_rcv

该函数是对不同的igmp报文进行区分处理的函数

/*处理ipv4 igmp 数据包

1、对数据包的ip头部进行合理性检测,只有检查通过的数据包才会进行后续操作

2、对于非igmp报文的数据包,则直接返回

3、对于v1、v2的igmp report报文,调用br_multicast_add_group进行处理

4、对于v3的igmp report报文,调用br_multicast_igmp3_report进行处理

5、对于igmp leave报文,调用br_multicast_leave_group进行处理

6、对于igmp查询报文,调用br_multicast_query进行处理

*/

static int br_multicast_ipv4_rcv(structnet_bridge *br,

struct net_bridge_port *port,

struct sk_buff *skb)

{

structsk_buff *skb2 = skb;

structiphdr *iph;

structigmphdr *ih;

unsignedlen;

unsignedoffset;

interr;

/*We treat OOM as packet loss for now. */

if(!pskb_may_pull(skb, sizeof(*iph)))

return-EINVAL;

iph= ip_hdr(skb);

if(iph->ihl < 5 || iph->version != 4)

return-EINVAL;

if(!pskb_may_pull(skb, ip_hdrlen(skb)))

return-EINVAL;

iph= ip_hdr(skb);

if(unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))

return-EINVAL;

if(iph->protocol != IPPROTO_IGMP)

return0;

len= ntohs(iph->tot_len);

if(skb->len < len || len < ip_hdrlen(skb))

return-EINVAL;

if(skb->len > len) {

skb2= skb_clone(skb, GFP_ATOMIC);

if(!skb2)

return-ENOMEM;

err= pskb_trim_rcsum(skb2, len);

if(err)

gotoerr_out;

}

len-= ip_hdrlen(skb2);

offset= skb_network_offset(skb2) + ip_hdrlen(skb2);

__skb_pull(skb2,offset);

skb_reset_transport_header(skb2);

err= -EINVAL;

if(!pskb_may_pull(skb2, sizeof(*ih)))

gotoout;

iph= ip_hdr(skb2);

switch(skb2->ip_summed) {

caseCHECKSUM_COMPLETE:

if(!csum_fold(skb2->csum))

break;

/*fall through */

caseCHECKSUM_NONE:

skb2->csum= 0;

if(skb_checksum_complete(skb2))

gotoout;

}

err= 0;

BR_INPUT_SKB_CB(skb)->igmp= 1;

ih= igmp_hdr(skb2);

switch(ih->type) {

caseIGMP_HOST_MEMBERSHIP_REPORT:

caseIGMPV2_HOST_MEMBERSHIP_REPORT:

BR_INPUT_SKB_CB(skb2)->mrouters_only= 1;

err= br_multicast_add_group(br, port, ih->group);

break;

caseIGMPV3_HOST_MEMBERSHIP_REPORT:

err= br_multicast_igmp3_report(br, port, skb2);

break;

caseIGMP_HOST_MEMBERSHIP_QUERY:

err= br_multicast_query(br, port, skb2);

break;

caseIGMP_HOST_LEAVE_MESSAGE:

br_multicast_leave_group(br,port, ih->group);

break;

}

out:

__skb_push(skb2,offset);

err_out:

if(skb2 != skb)

kfree_skb(skb2);

returnerr;

}

通过上面的分析,我们可以确认这个函数是个中转分配函数,对于不同类型的igmp报文,调用不同的协议处理函数。下面一一分析igmp report、igmp leave、igmp query报文。

三、igmp report报文的处理

对于igpm report报文,当接收到这个报文后,说明有一个桥端口要加入到一个组播组中,对于开启了igmp snooping时,需要完成以下操作:

1. 根据组播组ip地址,计算hash值,然后在组播组转发数据库表hash数组中找到相应的hash链表

2. 根据组播组ip地址,遍历hash链表,查找符合条件的组播组转发数据库项,此时存在两种情况

a)若查找到符合条件的组播组转发数据库项,则遍历该组播组转发数据库项,判断该桥端口是否已加入该组播组转发数据库项

i)若在组播组转发数据库项的组播端口链表中已经有该桥端口了,则更新组播端口的失效超时定时器

ii)若在组播组转发数据库项的组播端口链表中不存在该桥端口,则创建新的组播端口,并加入链表中,然后开启组播组端口的失效超时定时器

b)若查找不到符合条件的组播组转发数据库项,则创建一个新的组播组转发数据库项,并添加到组播组转发数据库表的hash链表中,

i)创建组播端口,加入到链表中,然后开启组播组端口的失效超时定时器。

Igmp report的处理流程大概就是上面的介绍,下面我们开始分析具体的函数。

首先在函数br_multicast_ipv4_rcv里,会将

BR_INPUT_SKB_CB(skb2)->mrouters_only设置为1,以便于发送给上层协议栈以及桥接wan侧端口。

然后调用函数br_multicast_add_group

下面分析这个函数

/*

功能:增加一个组播组数据库项,并将其链接到br的组播组数据库表的hash表中,

1、对于224.0.0.x的组播组地址,则不添加一个组播组数据库项

(因为224.0.0.x的地址一般是系统预留的,包括224.0.0.1代表子系统中的所有pc,

224.0.0.2代表子系统中的所有组播路由器)

2、调用br_multicast_new_group创建或者获取一个已存在的组播组数据库项

3、链表组播组数据库项的组播端口链表,查找要加入的组播端口是否已存在

a)若存在,则更新组播端口失效定时器的值

b)若不存在,则创建一个组播端口,并将其加入到组播组转发数据库项的组播端

口链表中,并更新组播端口的失效定时器的值。

*/

static intbr_multicast_add_group(struct net_bridge *br,

struct net_bridge_port *port, __be32 group)

{

struct net_bridge_mdb_entry *mp;

struct net_bridge_port_group *p;

struct net_bridge_port_group **pp;

unsigned long now = jiffies;

int err;

if (ipv4_is_local_multicast(group))

return 0;

spin_lock(&br->multicast_lock);

if (!netif_running(br->dev) ||

(port&& port->state == BR_STATE_DISABLED))

goto out;

mp = br_multicast_new_group(br, port, group);

err = PTR_ERR(mp);

if (unlikely(IS_ERR(mp) || !mp))

goto err;

if (!port) {

hlist_add_head(&mp->mglist,&br->mglist);

mod_timer(&mp->timer, now +br->multicast_membership_interval);

goto out;

}

for (pp = &mp->ports; (p = *pp); pp =&p->next) {

if (p->port == port)

goto found;

if ((unsigned long)p->port <(unsigned long)port)

break;

}

p = kzalloc(sizeof(*p), GFP_ATOMIC);

err = -ENOMEM;

if (unlikely(!p))

goto err;

p->addr = group;

p->port = port;

p->next = *pp;

hlist_add_head(&p->mglist,&port->mglist);

setup_timer(&p->timer, br_multicast_port_group_expired,

(unsigned long)p);

setup_timer(&p->query_timer,br_multicast_port_group_query_expired,

(unsigned long)p);

rcu_assign_pointer(*pp, p);

found:

mod_timer(&p->timer, now +br->multicast_membership_interval);

out:

err = 0;

err:

spin_unlock(&br->multicast_lock);

return err;

}

在这个函数的最后,代码会更新组播端口的失效超时定时器,如果在失效超时定时器超时了,还没有更新组播端口的失效超时定时器,则会调用函数

br_multicast_port_group_expired,删除该组播端口(待会分析这个函数)。

接下来分析函数br_multicast_new_group

/*

功能:向br桥的组播组数据库表中链接的组播组数据库项中,加入一个组播端口

1、首先判断br桥的组播组数据库表是否为空,为空则调用

br_mdb_rehash一个组播组数据库表

2、为该组播组ip计算hash值

3、调用br_multicast_get_group执行如下操作

a)查找一个已存在的组播组数据库项,若查找到,则直接返回

b)若没有查找到,则创建一个组播组数据库项。

i)若返回值为0,则创建一个组播组数据库项,并加入到组播组数据库表的hash数组中的链表中

ii)若返回-EAGAIN,则说明重新创建了组播组数据库表,此时则需要根据新的组播组数据库表

计算新的hash值,然后再执行i的操作

*/

static struct net_bridge_mdb_entry*br_multicast_new_group(

structnet_bridge *br, struct net_bridge_port *port, __be32 group)

{

structnet_bridge_mdb_htable *mdb = br->mdb;

structnet_bridge_mdb_entry *mp;

inthash;

if(!mdb) {

if(br_mdb_rehash(&br->mdb, BR_HASH_SIZE, 0))

returnNULL;

gotorehash;

}

hash= br_ip_hash(mdb, group);

mp= br_multicast_get_group(br, port, group, hash);

switch(PTR_ERR(mp)) {

case0:

break;

case-EAGAIN:

rehash:

mdb= br->mdb;

/*计算hash值*/

hash= br_ip_hash(mdb, group);

break;

default:

gotoout;

}

mp= kzalloc(sizeof(*mp), GFP_ATOMIC);

if(unlikely(!mp))

gotoout;

mp->br= br;

mp->addr= group;

setup_timer(&mp->timer,br_multicast_group_expired,

(unsigned long)mp);

setup_timer(&mp->query_timer,br_multicast_group_query_expired,

(unsigned long)mp);

hlist_add_head_rcu(&mp->hlist[mdb->ver],&mdb->mhash[hash]);

mdb->size++;

out:

returnmp;

}

在这个函数里有两个函数比较重要:br_multicast_get_group、br_mdb_rehash

下面首先分析函数br_multicast_get_group

/*

功能:从一个br桥的组播组数据库表中查找一个组播组数据库项

1、若查找到函数直接返回

2、若没有查找到,则执行如下操作

a)若组播组数据库表的hash表mdb->[mhash]中已经链接的组播组数据库的值大于

br->hash_elasticity,则会调用br_mdb_rehash重新计算hash,重新创建新的组播组数据库表

i)若调用成功,则 返回-EAGAIN,表示可以创建新的组播组数据库项,且组播组数据库表已更新,

需要计算新的hash值

ii)若调用不成功,则关闭igpmsnooping,并返回其他错误信息

b)若组播组数据库表的所有hash表中,已经链接的组播组数据库项的值

大于mdb->max,则将hash数组的值扩大至2倍

i)若扩大后的值大于br->hash_max的值,则关闭igmp snooping,并返回错误信息

ii)若扩大后的值不大于br->hash_max,则调用br_mdb_rehash重新创建组播组数据库表,成功后

返回-EAGAIN;否则关闭igmp snooping,flood所有组播流数据

c)若不属于以上两种情况,则直接返回NULL,表示可以创建新的组播组数据库项,且组播组数据库表没有更新

对于这个函数,我感觉存在以下问题:

1、当所有hash表项中的组播组数据库项的总数大于hash数组的最大值时,则

会将hash数组的容量扩大一倍,按照这个实现的话,作者是想一个hash链表中

最大只有一个链表节点,但是我们通过br->hash_elasticity设置了一个hash表中

最大可以有几个hash节点,我感觉此处可以将判断 mdb->size >= max改成

mdb->size >= max*br->hash_elasticity,我感觉这样比较合理一点。

该函数巧妙的一点,当通过执行代码,确定不能添加新的组播组数据库项时,

函数不是直接返回失败,而是先把igmp snooping先关闭,然后再返回,这样

一来,所有的组播组flood到所有桥端口,组播功能还能起作用。

如果不关闭igmp snooping,也不创建新的组播组数据库项,则原来已加入的

端口还均能接收到组播流,而想要新加入的那个桥端口则收不到新的组播流,

会影响功能。

*/

static struct net_bridge_mdb_entry*br_multicast_get_group(

structnet_bridge *br, struct net_bridge_port *port, __be32 group,

inthash)

{

structnet_bridge_mdb_htable *mdb = br->mdb;

structnet_bridge_mdb_entry *mp;

structhlist_node *p;

unsignedcount = 0;

unsignedmax;

intelasticity;

interr;

hlist_for_each_entry(mp,p, &mdb->mhash[hash], hlist[mdb->ver]) {

count++;

if(unlikely(group == mp->addr)) {

returnmp;

}

}

elasticity= 0;

max= mdb->max;

if(unlikely(count > br->hash_elasticity && count)) {

if(net_ratelimit())

printk(KERN_INFO"%s: Multicast hash table "

"chain limit reached: %s\n",

br->dev->name, port ?port->dev->name :

br->dev->name);

elasticity= br->hash_elasticity;

}

/*通过这个判断,感觉是当组播组数据库项的成员

大于*/

if(mdb->size >= max) {

max*= 2;

if(unlikely(max >= br->hash_max)) {

printk(KERN_WARNING"%s: Multicast hash table maximum "

"reached, disabling snooping: %s,%d\n",

br->dev->name, port ?port->dev->name :

br->dev->name,

max);

err= -E2BIG;

disable:

br->multicast_disabled= 1;

gotoerr;

}

}

if(max > mdb->max || elasticity) {

if(mdb->old) {

if(net_ratelimit())

printk(KERN_INFO"%s: Multicast hash table "

"on fire: %s\n",

br->dev->name, port ?port->dev->name :

br->dev->name);

err= -EEXIST;

gotoerr;

}

err= br_mdb_rehash(&br->mdb, max, elasticity);

if(err) {

printk(KERN_WARNING"%s: Cannot rehash multicast "

"hash table, disabling snooping:"

"%s, %d, %d\n",

br->dev->name, port ?port->dev->name :

br->dev->name,

mdb->size, err);

gotodisable;

}

err= -EAGAIN;

gotoerr;

}

returnNULL;

err:

mp= ERR_PTR(err);

returnmp;

}

下面我们分析函数br_mdb_rehash

/*

功能:组播组转发数据库表的初始化或者重新初始化

1、初始化

为组播组转发数据库表申请内存,

为组播组转发数据库表中的hash数组申请内存

设置mdb->max、mdb->size的值

2、重新初始化

对于已经存在的组播组数据库表,则重新申请内存进行初始化操作

除了执行上述1的操作外,还需要将原组播组数据库表中hash数组里所有的

组播组数据库项,经过重新计算hash值后,重新链接到新的组播组数据库表

的hash数组里的hash链表中,最后调用br_mdb_free释放掉原组播组数据库表的内存

*/

static int br_mdb_rehash(structnet_bridge_mdb_htable **mdbp, int max,

int elasticity)

{

structnet_bridge_mdb_htable *old = *mdbp;

structnet_bridge_mdb_htable *mdb;

interr;

mdb= kmalloc(sizeof(*mdb), GFP_ATOMIC);

if(!mdb)

return-ENOMEM;

mdb->max= max; //组播组数据库表中hash数组的最大值

mdb->old= old;

/*申请内存,创建hash数组*/

mdb->mhash= kzalloc(max * sizeof(*mdb->mhash), GFP_ATOMIC);

if(!mdb->mhash) {

kfree(mdb);

return-ENOMEM;

}

mdb->size= old ? old->size : 0;

mdb->ver= old ? old->ver ^ 1 : 0;

if(!old || elasticity)

get_random_bytes(&mdb->secret,sizeof(mdb->secret));

else

mdb->secret= old->secret;

if(!old)

gotoout;

err= br_mdb_copy(mdb, old, elasticity);

if(err) {

kfree(mdb->mhash);

kfree(mdb);

returnerr;

}

call_rcu_bh(&mdb->rcu,br_mdb_free);

out:

rcu_assign_pointer(*mdbp,mdb);

return0;

}

上面几个函数吧组播组转发数据库表初始化、组播组转发数据库项的创建与查找、组播端口的创建都介绍了。刚才我们分析了组播端口的失效超时定时器的超时处理函数br_multicast_port_group_expired,下面我们分析这个函数。

/*

功能:组播端口的失效定时器的超时执行函数

该函数的功能比较简单,调用br_multicast_del_pg删除组播端口

*/

static voidbr_multicast_port_group_expired(unsigned long data)

{

struct net_bridge_port_group *pg = (void*)data;

struct net_bridge *br = pg->port->br;

spin_lock(&br->multicast_lock);

if (!netif_running(br->dev) ||timer_pending(&pg->timer) ||

hlist_unhashed(&pg->mglist))

goto out;

br_multicast_del_pg(br, pg);

out:

spin_unlock(&br->multicast_lock);

}

接着分析 br_multicast_del_pg

/*

功能:删除一个组播端口

1、调用br_mdb_ip_get查找到该组播端口关联的组播组数据库项

2、将该组播端口从关联组播组数据库项的组播端口链表中删除

3、删除组播端口的失效定时器和查询定时

4、调用br_multicast_free_pg,释放该组播端口占用的内存

5、若该组播端口关联的组播组数据库项的组播端口链表上已经没有组播端口

,则更新该组播组数据库项的失效超时时间为当前时间,即组播组数据库

的失效超时定时器立马到期,执行其超时处理函数。

对于组播组数据库项与组播端口的数据结构定义,我有些不懂,通过组播

端口查找其所关联的组播组数据库项,是通过组播端口的addr成员,经过遍历链表

实现的,为什么不直接在组播端口的数据结构中增加一个指针指向其所关联的组播组

数据库呢,这样不是可以很快查找到吗? 我感觉完全可以将组播端口中的addr成员替换为

一个指针用于指向其所关联的组播组数据库项。

*/

static voidbr_multicast_del_pg(struct net_bridge *br,

structnet_bridge_port_group *pg)

{

struct net_bridge_mdb_htable *mdb = br->mdb;

struct net_bridge_mdb_entry *mp;

struct net_bridge_port_group *p;

struct net_bridge_port_group **pp;

mp = br_mdb_ip_get(mdb, pg->addr);

if (WARN_ON(!mp))

return;

for (pp = &mp->ports; (p = *pp); pp =&p->next) {

if (p != pg)

continue;

*pp = p->next;

hlist_del_init(&p->mglist);

del_timer(&p->timer);

del_timer(&p->query_timer);

call_rcu_bh(&p->rcu,br_multicast_free_pg);

if (!mp->ports && hlist_unhashed(&mp->mglist)&&

netif_running(br->dev))

mod_timer(&mp->timer,jiffies);

return;

}

WARN_ON(1);

}

此处函数br_multicast_free_pg会释放组播端口数据结构占用的内存

/*

功能:释放一个组播端口所占的内存

*/

static voidbr_multicast_free_pg(struct rcu_head *head)

{

struct net_bridge_port_group *p =

container_of(head, structnet_bridge_port_group, rcu);

kfree(p);

}

此处如果组播组转发数据库项的组播端口链表中,如果没有组播端口时,就会让组播组转发数据库项的失效定时器超时,从而执行组播组转发数据库项的失效超时处理函数。下面我们分析一下组播组转发数据库项的失效超时处理函数br_multicast_group_expired

/*

功能:取消一个组播组数据库项与组播组数据库表的关联,并释放

该组播组数据库项所占的内存

*/

static voidbr_multicast_group_expired(unsigned long data)

{

struct net_bridge_mdb_entry *mp = (void *)data;

struct net_bridge *br = mp->br;

struct net_bridge_mdb_htable *mdb;

spin_lock(&br->multicast_lock);

if (!netif_running(br->dev) ||timer_pending(&mp->timer))

goto out;

if (!hlist_unhashed(&mp->mglist))

hlist_del_init(&mp->mglist);

/*

判断该组播组数据项里是否有端口成员,

a)若有则不释放该组播组数据项

b)若没有,则将该数据项从组播组数据库表的hash表中删除

并删除该组播组数据库项的查询定时器

*/

if (mp->ports)

goto out;

mdb = br->mdb;

hlist_del_rcu(&mp->hlist[mdb->ver]);

mdb->size–;

del_timer(&mp->query_timer);

call_rcu_bh(&mp->rcu,br_multicast_free_group);

out:

spin_unlock(&br->multicast_lock);

}

此处则调用br_multicast_free_group释放组播组转发数据库项占用的内存。

在添加组播组转发数据库项和组播端口时,会建立失效超时定时器和查询定时器。

但是,我们发现在创建完这些定时器后,只有组播端口失效超时定时器是启动的,其他三个定时器并没有启动。这是为什么呢?

对于组播组转发数据库项的失效超时定时器以及查询定时器,只有收到leave报文时才会启动,我们在下面介绍leave报文时介绍。

那只启动了组播端口的失效超时定时器,而没有启动组播端口的查询定时器,那不就不会对该端口发送查询报文了吗,那组播端口岂不会过一段时间久被删除掉了呢?

答案是不会的,因为在上一节igmp snooping初始化时,有分析道当桥端口up起来且开启igmp snooping时,就会启动桥端口的组播查询定时器(该定时器的超时时间设置为br->multicast_startup_query_interval或者br-> multicast_query_interval,是小于组播端口的失效超时时间br->multicast_membership_interval的),在超时后就会发送通用组播查询报文,这样如果该桥端口下有加入的组播组,就会回复igmp report报文,然后就会更新组播端口的失效超时定时器,这样就不会随便删除报文了。

这次分析完了组播组 igmp report的处理过程,以及组播端口的失效超时定时器与桥端口的查询定时器之间的关系及功能。

四、igmp leave报文的处理

在函数br_multicast_ipv4_rcv里是调用br_multicast_leave_group来处理leave报文的,下面分析一下这个函数br_multicast_leave_group

/*

功能:igmp leave 报文的处理函数

1、对于组播组地址为224.0.0.x的组播报文不进行处理

2、根据组播组地址,从组播组转发数据库表的hash链表中,查找符合

条件的组播组转发数据库项

i)若没有找到则直接返回

ii)若查找到,则执行第三步

3、若没有桥端口设备,则开启组播组转发数据库项的失效超时定时器和

查询定时器,程序返回

4、若有相应的桥端口设备,则在组播组转发数据库项的组播端口链表中

查找符合条件的组播端口

i)若查找到,则更新组播端口的失效超时时间,并将组播端口的查询

定时器设置为马上失效

ii)若没有查找到,则直接返回

*/

static void br_multicast_leave_group(structnet_bridge *br,

struct net_bridge_port *port,

__be32 group)

{

structnet_bridge_mdb_htable *mdb;

structnet_bridge_mdb_entry *mp;

structnet_bridge_port_group *p;

unsignedlong now;

unsignedlong time;

if(ipv4_is_local_multicast(group))

return;

spin_lock(&br->multicast_lock);

if(!netif_running(br->dev) ||

(port && port->state ==BR_STATE_DISABLED) ||

timer_pending(&br->multicast_querier_timer))

goto out;

mdb= br->mdb;

mp= br_mdb_ip_get(mdb, group);

if(!mp)

gotoout;

now= jiffies;

/*设置组播组数据库项的超时时间*/

time= now + br->multicast_last_member_count *

br->multicast_last_member_interval;

if(!port) {

if(!hlist_unhashed(&mp->mglist) &&

(timer_pending(&mp->timer) ?

time_after(mp->timer.expires, time) :

try_to_del_timer_sync(&mp->timer)>= 0)) {

mod_timer(&mp->timer,time);

mp->queries_sent= 0;

mod_timer(&mp->query_timer,now);

}

gotoout;

}

for(p = mp->ports; p; p = p->next) {

if(p->port != port)

continue;

if(!hlist_unhashed(&p->mglist) &&

(timer_pending(&p->timer) ?

time_after(p->timer.expires, time) :

try_to_del_timer_sync(&p->timer)>= 0)) {

mod_timer(&p->timer,time);

p->queries_sent= 0;

mod_timer(&p->query_timer,now);

}

break;

}

out:

spin_unlock(&br->multicast_lock);

}

当这个函数执行完,如果找到相应的组播组转发数据库项或者组播端口,则主要是启动或者修改失效超时定时器与查询定时器。

因为我们接收到的igmp leave报文,均是从lan侧设备发送的,所以调用完该函数后就重置了组播端口的失效超时定时器和查询定时器。

对于组播端口与组播组转发数据库项的失效超时处理函数,在上面已经介绍了,下面介绍组播端口的查询超时处理函数br_multicast_port_group_query_expired

/*

功能:组播端口查询定时器的超时执行函数

*/

static voidbr_multicast_port_group_query_expired(unsigned long data)

{

structnet_bridge_port_group *pg = (void *)data;

structnet_bridge_port *port = pg->port;

structnet_bridge *br = port->br;

spin_lock(&br->multicast_lock);

if(!netif_running(br->dev) || hlist_unhashed(&pg->mglist) ||

pg->queries_sent >=br->multicast_last_member_count)

gotoout;

br_multicast_send_port_group_query(pg);

out:

spin_unlock(&br->multicast_lock);

}

该函数主要是调用br_multicast_send_port_group_query发送组播查询报文,下面分析一下这个函数

static voidbr_multicast_send_port_group_query(struct net_bridge_port_group *pg)

{

structnet_bridge_port *port = pg->port;

structnet_bridge *br = port->br;

structsk_buff *skb;

skb= br_multicast_alloc_query(br, pg->addr);

if(!skb)

gototimer;

br_deliver(port,skb);

timer:

if(++pg->queries_sent < br->multicast_last_member_count)

mod_timer(&pg->query_timer,

jiffies +br->multicast_last_member_interval);

}

这个函数的流程一目了然,首先创建一个特定组播组查询报文,然后调用br_deliver从相应的端口发送出去,最后修改组播端口的查询定时器。

下面我们再看组播组转发数据库项的查询超时处理函数

static voidbr_multicast_group_query_expired(unsigned long data)

{

structnet_bridge_mdb_entry *mp = (void *)data;

structnet_bridge *br = mp->br;

spin_lock(&br->multicast_lock);

if(!netif_running(br->dev) || hlist_unhashed(&mp->mglist) ||

mp->queries_sent >=br->multicast_last_member_count)

gotoout;

br_multicast_send_group_query(mp);

out:

spin_unlock(&br->multicast_lock);

}

该函数在做了合理性检查后,即会调用br_multicast_send_group_query进行后续的处理。那我们分析一下函数br_multicast_send_group_query

static voidbr_multicast_send_group_query(struct net_bridge_mdb_entry *mp)

{

structnet_bridge *br = mp->br;

structsk_buff *skb;

skb= br_multicast_alloc_query(br, mp->addr);

if(!skb)

gototimer;

netif_rx(skb);

timer:

if(++mp->queries_sent < br->multicast_last_member_count)

mod_timer(&mp->query_timer,

jiffies +br->multicast_last_member_interval);

}

该函数首先调用br_multicast_alloc_query发送一个特定查询报文,然后调用netif_rx经过软中断处理后交予上层协议栈进行后续处理。最后更新组播组转发数据库项的查询定时器。

我认为本地接口br0不会发送igmp leave报文,所以组播组转发数据库项的失效超时定时器与查询定时器并不会触发,而组播端口的查询定时器,在接收到igmp v2、v3的leave报文后才会触发。

即使lan侧设备永远不发送igmp leave报文,即永远不会触发组播组转发数据库项的失效超时定时器与查询定时,以及组播端口的查询定时器,但有桥端口的查询超时定时器与组播端口的失效超时定时器一起配合工作,同样能对组播组转发数据库表进行更新。

五、igmp query报文的处理

对于接收到igmp query报文,主要有两个途径

1) 本地上层协议开启igmp proxy功能,向br桥发送通用或者特定组播组查询报文

2) 桥中有一个桥接wan连接端口,该端口会接收到通用或者特定组播组查询报文

对于第一种情况,我们需要更新组播端口的失效超时定时器,而对于第二种情况,我们需要将这个桥端口加入到br->route_list链表中,以便桥接收到组播report报文后,将这些报文转发到这些桥端口中。

下面我们分析一下组播查询报文的处理函数br_multicast_query

/*

IGMP查询报文一般是组播路由器创建并发送的报文,因为组播路

由器要维护组播路由表,因此需要定期发送通用查询或者特定组成员查询。

接收到的igmp 查询报文有两种情况:

1、开启igmp proxy,此时上层协议会通过桥接口将igmp 查询报文发送到lan侧

所有端口中

2、当没有开启igmp proxy,要想要桥端口能够收到组播端口,则其中有一个端口

会组播桥接wan端口连接到一个组播路由器的子网中,此时这个桥接端口则会

接收到igmp 查询报文,这时就需要将该桥接端口通过hash节点链接到

网桥的hash表头router_list.first中去。

*/

static intbr_multicast_query(struct net_bridge *br,

struct net_bridge_port *port,

struct sk_buff *skb)

{

struct iphdr *iph = ip_hdr(skb);

struct igmphdr *ih = igmp_hdr(skb);

struct net_bridge_mdb_entry *mp;

struct igmpv3_query *ih3;

struct net_bridge_port_group *p;

struct net_bridge_port_group **pp;

unsigned long max_delay;

unsigned long now = jiffies;

__be32 group;

int err = 0;

spin_lock(&br->multicast_lock);

if (!netif_running(br->dev) ||

(port&& port->state == BR_STATE_DISABLED))

goto out;

br_multicast_query_received(br, port,iph->saddr);

group = ih->group;

if (skb->len == sizeof(*ih)) {

max_delay = ih->code * (HZ /IGMP_TIMER_SCALE);

if (!max_delay) {

max_delay = 10 * HZ;

group = 0;

}

} else {

if (!pskb_may_pull(skb, sizeof(structigmpv3_query))) {

err = -EINVAL;

goto out;

}

ih3 = igmpv3_query_hdr(skb);

if (ih3->nsrcs)

goto out;

max_delay = ih3->code ?

IGMPV3_MRC(ih3->code) * (HZ /IGMP_TIMER_SCALE) : 1;

}

if (!group)

goto out;

mp = br_mdb_ip_get(br->mdb, group);

if (!mp)

goto out;

max_delay *=br->multicast_last_member_count;

if (!hlist_unhashed(&mp->mglist)&&

(timer_pending(&mp->timer) ?

time_after(mp->timer.expires, now + max_delay) :

try_to_del_timer_sync(&mp->timer) >= 0))

mod_timer(&mp->timer, now +max_delay);

for (pp = &mp->ports; (p = *pp); pp =&p->next) {

if (timer_pending(&p->timer) ?

time_after(p->timer.expires, now + max_delay) :

try_to_del_timer_sync(&p->timer) >= 0)

mod_timer(&mp->timer, now+ max_delay);

}

out:

spin_unlock(&br->multicast_lock);

return err;

}

该函数首先会调用br_multicast_query_received,对于桥端口不为空的情况,将桥端口加入到br->route_list中,并开启桥端口的路由查询定时器以及桥的查询定时器。

接着对于特定组播组查询报文,会根据组播组地址查找相应的组播组转发数据库项,更新所有相关的组播端口的失效超时定时器。

对于桥端口的路由查询定时器,如果在查询定时器超时前没有再从这个桥端口收到查询报文,则会将该桥端口从br->route_list中删除(通过桥端口的路由查询定时器处理函数br_multicast_router_expired)

下面是这个函数的定义,该函数的流程还是比较简单的。

static voidbr_multicast_router_expired(unsigned long data)

{

structnet_bridge_port *port = (void *)data;

structnet_bridge *br = port->br;

spin_lock(&br->multicast_lock);

if(port->multicast_router != 1 ||

timer_pending(&port->multicast_router_timer) ||

hlist_unhashed(&port->rlist))

gotoout;

hlist_del_init_rcu(&port->rlist);

out:

spin_unlock(&br->multicast_lock);

}

至此,基本完成了igmp 报文的处理流程,下一节分析下igmpsnooping子系统与网桥子层之间的关系。

绚丽的民族风情,悠久的历史文化。抛开尘世的纷扰,

Linux IGMP SNOOPING 学习笔记 之二 igmp数据包处理

相关文章:

你感兴趣的文章:

标签云: