IP组播

一、IGMP主机发送报告
1、用户层客户端实现加入组播组的代码片段。
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(MCAST_ADDR);  /*多播地址*/
//这里当网络接口为默认的时候并不是指绑定任何接口的意思,而是根据路由表查找当前的
//多播地址是否含有路由项,之后使用该路由项的出口设备做为接口,在有此系统上没有添
//多默认的多播路由,会导致这里使用默认时,出现查找路由失败,而返回
//ENODEV(No Such device)的错误。
mreq.imr_interface.s_addr = htonl(INADDR_ANY);      /*网络接口为默认*/
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

内核对IP_ADD_MEMBERSHIP选项的处理:
ip_mc_join_group(sk, &mreq);
addr = imr->imr_multiaddr.s_addr;
inet_sock *inet = inet_sk(sk);

//查找该多播地址设置到哪个接口设备。
//1、如果用户传入了接口索引,则使用接口索引进行查找
//2、否则如果用户传入了接口的地址,则使用地址进行查找
//3、否则使用多播地址进行路由表查找。
in_dev = ip_mc_find_dev(imr);

//查找当前套接口是否已经加入了该多播组。如果是,则直接返回。
ifindex = imr->imr_ifindex;
for (i = inet->mc_list; i; i = i->next)
if (i->multi.imr_multiaddr.s_addr == addr &&i->multi.imr_ifindex == ifindex)
goto done;
count++;

//一个套接口仅能加入20个多播组,超过阀值则报错。
err = -ENOBUFS;
if (count >= sysctl_igmp_max_memberships)
goto done;

iml = sock_kmalloc(sk,sizeof(*iml),GFP_KERNEL);

//将该多播组信息加入到inet套接口对象的mc_list中
memcpy(&iml->multi, imr, sizeof(*imr));
iml->next = inet->mc_list;
iml->sflist = NULL;				//初始源过滤表空
iml->sfmode = MCAST_EXCLUDE;	//初始源过滤模式为EXCLUDE(类似黑名单)
inet->mc_list = iml;

//将组播加入到设备接口的组播列表中
ip_mc_inc_group(in_dev, addr);
for (im=in_dev->mc_list; im; im=im->next)
if (im->multiaddr == addr)
//如果当前接口已加入该多播组,则增加引用计数
im->users++;

//这里入参的过滤源计数为0,如果过滤模式为EXCLUDE,通常表示
//加入组播组。
ip_mc_add_src(in_dev, &addr, MCAST_EXCLUDE, 0, NULL, 0);
//从mc_list中找到匹配该多播地址的列表项
for (pmc=in_dev->mc_list; pmc; pmc=pmc->next)
if (*pmca == pmc->multiaddr)
break;

//对所有过滤源进行标记,先标记出当前过滤源的模式是否与组的
//过滤模式相反。这里主要用了和下面的sf_setstate配合检测是否
//有过滤源变动。
sf_markstate(pmc);
//当前多播组EXCLUDE过滤模式的计数
mca_xcount = pmc->sfcount[MCAST_EXCLUDE];

//1、如果组为EXCLUDE过滤模式
//	当前过滤源的并且不能含有INCLUDE过滤模式,并且当前
//   过滤源的EXCLUDE过滤模式的条目必须和组的
//	EXCLUDE条目数相同才为TRUE。见如下例子。
//	假设第1次向224.1.1.1组加入了一个过滤源1.1.1.1,模式
//	为EXCLUDE,这时候,组的EXCLUDE数为1,过滤源
//	1.1.1.1的EXCLUDE数为1,此时满足这个条件。这时候
//	再次向224.1.1.1组加入一个过滤源2.2.2.2,模式为
//	EXCLUDE,这时候,组的EXCLUDE数为2,过滤源1.1.1.1
//	的EXCLUDE数仍为1,过滤源2.2.2.2的EXCLUDE数为
//	1,此时这两个过滤源都不会满足此条件。
//2、如果组为INCLUDE过滤模式,则根据当前过滤源中是否含	//	有INCLUDE的计数来标记。
for (psf=pmc->sources; psf; psf=psf->sf_next)
if (pmc->sfcount[MCAST_EXCLUDE])
psf->sf_oldin = mca_xcount ==
psf->sf_count[MCAST_EXCLUDE] &&
!psf->sf_count[MCAST_INCLUDE];
else
psf->sf_oldin = psf->sf_count[MCAST_INCLUDE] != 0;

//当前加入源过滤的模式是否是EXCLUDE
isexclude = pmc->sfmode == MCAST_EXCLUDE;

//更新组的当前过滤源模式计数
if (!delta)
pmc->sfcount[sfmode]++;

//将过滤源加入到源列表中,如果当前源是第一次加入,则需要更新
//路由缓存。
for (i=0; isfcount[MCAST_EXCLUDE] != 0),之后比较当前组
//过滤模式是否与传入的过滤模式不同。
if (isexclude != (pmc->sfcount[MCAST_EXCLUDE] != 0))
//根据是否含有EXCLUDE来决定当前组过滤模式是否为
//EXCLUDE。
if (pmc->sfcount[MCAST_EXCLUDE])
pmc->sfmode = MCAST_EXCLUDE;
else if (pmc->sfcount[MCAST_INCLUDE])
pmc->sfmode = MCAST_INCLUDE;

//更新组的健壮值,用于重发多少次报告
pmc->crcount = in_dev->mr_qrv ? in_dev->mr_qrv :
IGMP_Unsolicited_Report_Count;

in_dev->mr_ifc_count = pmc->crcount;

//将源的健壮值,表明不需要报告指定源的信息
for (psf=pmc->sources; psf; psf = psf->sf_next)
psf->sf_crcount = 0;

//IGMP V3的组播改变事件处理
igmp_ifc_event(in_dev);
//IGMP V1/V2版本不支持过滤源的上报。
if (IGMP_V1_SEEN(in_dev) || IGMP_V2_SEEN(in_dev))
return;

//接口改变定时器更新
in_dev->mr_ifc_count = in_dev->mr_qrv ? in_dev->mr_qrv :
IGMP_Unsolicited_Report_Count;

//启动in_dev->mr_ifc_timer定时器,该定时器的超时
//处理函数为igmp_ifc_timer_expire,是在ip_mc_init_dev
//初始化时设置的。定时器超时函数下面单独分析。
igmp_ifc_start_timer(in_dev, 1);
int tv = net_random() % delay;

mod_timer(&in_dev->mr_ifc_timer, jiffies+tv+2)

//定时器引用了in_dev对象,后面在定时器超时处理时
//会看到释放这次引用。
in_dev_hold(in_dev);

//当前组的过滤模式没有改变
else
rv = sf_setstate(pmc)
//组含有EXCLUDE模式的计数
mca_xcount = pmc->sfcount[MCAST_EXCLUDE];

//查询者的健壮变量
qrv = pmc->interface->mr_qrv;

//默认返回值为0
rv = 0;

for (psf=pmc->sources; psf; psf=psf->sf_next)
//这里的判断方法同上面sf_markstate处理相同,
//检测最新过滤源是否与组的过滤模式相同。
if (pmc->sfcount[MCAST_EXCLUDE])
new_in = 
mca_xcount == psf->sf_count[MCAST_EXCLUDE] 
&&!psf->sf_count[MCAST_INCLUDE];
else
new_in = psf->sf_count[MCAST_INCLUDE] != 0;

if (new_in)
//如果当前源新的过滤模式与组的过滤模式相同,
//同时以前老的过滤模式与组的不同,则需要将
//当前源从阻塞列表中删除。
if (!psf->sf_oldin)
for (dpsf=pmc->tomb; dpsf; 
dpsf=dpsf->sf_next)
if (dpsf->sf_inaddr == psf->sf_inaddr)
break;
prev = dpsf;
if (dpsf)
if (prev)
prev->sf_next = dpsf->sf_next;
else
pmc->tomb = dpsf->sf_next;
kfree(dpsf);
//设置源的健壮变量,后面需要上报源信息
psf->sf_crcount = qrv;
//表明有源变动。
rv++;
//否则当前新的源过滤模式与组的不同,同时该源之前
//的过滤模式与组相同。
else if (psf->sf_oldin)
psf->sf_crcount = 0;

//将当前源复制一份到阻塞列表中。
for (dpsf=pmc->tomb; dpsf; dpsf=dpsf->sf_next)
if (dpsf->sf_inaddr == psf->sf_inaddr)
break;
if (!dpsf)
dpsf = (struct ip_sf_list *)
kmalloc(sizeof(*dpsf), GFP_ATOMIC);

*dpsf = *psf;
dpsf->sf_next = pmc->tomb;
pmc->tomb = dpsf;

//设置源的健壮变量,后面需要上报源信息
dpsf->sf_crcount = qrv;
//表明有源变动。
rv++;

//如果有源变量,则发送IGMP消息。
if ( rv == true )
igmp_ifc_event(in_dev)

goto out;

im = kmalloc(sizeof(*im), GFP_KERNEL);
im->users=1;
im->interface=in_dev;	//该组关联的接口
in_dev_hold(in_dev);
im->multiaddr=addr;	//组播地址
im->sfmode = MCAST_EXCLUDE;	//初始过滤源模式
im->sfcount[MCAST_INCLUDE] = 0;
im->sfcount[MCAST_EXCLUDE] = 1;
im->sources = NULL;				//初始过滤源列表
im->tomb = NULL;				//初始阻塞源列表
im->crcount = 0;					//健壮值
atomic_set(&im->refcnt, 1);
spin_lock_init(&im->lock);
im->tm_running=0;
init_timer(&im->timer);					//过滤组报告定时器
im->timer.data=(unsigned long)im;
im->timer.function=&igmp_timer_expire;
im->unsolicit_count = IGMP_Unsolicited_Report_Count;
im->reporter = 0;
im->gsquery = 0;
im->loaded = 0;

//将新建的多播组对象加入到设备多播列表中。
im->next=in_dev->mc_list;
in_dev->mc_list=im;

//如果当前设备列表中对匹配的被阻塞的多播对象以及该多播对象下有被阻塞的
//多播过滤源,则从对应列表中删除。
igmpv3_del_delrec(in_dev, im->multiaddr);

//将多播地址添加到设备对象中,并更新物理网卡驱动的多播列表。
//同时启动多播定时器准备上报IGMP报告。
igmp_group_added(im);
struct in_device *in_dev = im->interface;

//仅加载一次
if (im->loaded == 0)
im->loaded = 1;
ip_mc_filter_add(in_dev, im->multiaddr);
struct net_device *dev = in_dev->dev;
//根据多播地址转换成MAC地址
//0x01005eXXXXXX
arp_mc_map(addr, buf, dev, 0)

//向设置对象中添加多播地址
dev_mc_add(dev,buf,dev->addr_len,0);
dmi1 = kmalloc(sizeof(*dmi), GFP_ATOMIC);

//检测到已存在则仅增加引用计数。
for (dmi = dev->mc_list; dmi != NULL; dmi = dmi->next)
if (memcmp(dmi->dmi_addr, addr, dmi->dmi_addrlen) == 
0 &&dmi->dmi_addrlen == alen)
dmi->dmi_users++;
goto done;

//将新建的多播对象加入到设备对象的多播列表中。
memcpy(dmi->dmi_addr, addr, alen);
dmi->dmi_addrlen = alen;
dmi->next = dev->mc_list;
dmi->dmi_users = 1;
dmi->dmi_gusers = 0
dev->mc_list = dmi;
dev->mc_count++;

//更新物理设备的多播列表
__dev_mc_upload(dev);
//设备未启动则暂不添加
if (!(dev->flags&IFF_UP))
return;

//设备驱动没有设置多播列表的接口函数,或者设备
//还未激活,则不添加 。
if (dev->set_multicast_list == NULL ||
!netif_device_present(dev))
return;

//调用设备驱动的接口函数进行物理设备的处理。
dev->set_multicast_list(dev);

//224.0.0.1的所有主机地址不进行IGMP报告
if (im->multiaddr == IGMP_ALL_HOSTS)
return;

//设备已经销毁,不进行IGMP报告
if (in_dev->dead)
return;

//V1/V2版本使用im->timer定时器触发事件报告,该定时器的回调函数为
//igmp_timer_expire
if (IGMP_V1_SEEN(in_dev) || IGMP_V2_SEEN(in_dev))
igmp_start_timer(im, IGMP_Initial_Report_Delay);
int tv=net_random() % max_delay;
im->tm_running=1;
if (!mod_timer(&im->timer, jiffies+tv+2))
//定时器引用了im对象,后面在定时器超时处理时会看到
//释放这次引用。
atomic_inc(&im->refcnt);
return;

im->crcount = in_dev->mr_qrv ? in_dev->mr_qrv :
IGMP_Unsolicited_Report_Count;

//V3版本使用in_dev->mr_ifc_timer定时器触发事件报告,该定时器的回
//调函数为igmp_ifc_timer_expire
igmp_ifc_event(in_dev);

//设备未销毁,则进行路由缓冲刷新。
if (!in_dev->dead)
ip_rt_multicast_event(in_dev);

----------------------------------------------------------------------------------------------------------------------
//IGMP V3接口改变定时器超时处理函数
igmp_ifc_timer_expire
//发送IGMPv3的改变报告
igmpv3_send_cr(in_dev);
//遍历所有离开的组
for (pmc=in_dev->mc_tomb; pmc; pmc=pmc_next)
pmc_next = pmc->next;

//如果之前组的过滤模式为INCLUDE,则需要将所有过滤源报告都需要被
//阻塞。这里两次调用add_grec,其中第4个参数分别为0/1是为了将该组
//的pmc->tomb及pmc->sources都进行收集。
if (pmc->sfmode == MCAST_INCLUDE)
type = IGMPV3_BLOCK_OLD_SOURCES;
dtype = IGMPV3_BLOCK_OLD_SOURCES;
skb = add_grec(skb, pmc, type, 1, 0);
skb = add_grec(skb, pmc, dtype, 1, 1);

if (pmc->crcount)
//如果该离开组的过滤模式不是INCLUDE,而是EXCLUDE,则报
//告TO_INCLUDE{},表明组离开。
if (pmc->sfmode == MCAST_EXCLUDE)
type = IGMPV3_CHANGE_TO_INCLUDE;
skb = add_grec(skb, pmc, type, 1, 0);

pmc->crcount--;
if (pmc->crcount == 0)
//清除当前组的所有阻塞源
igmpv3_clear_zeros(&pmc->tomb);
igmpv3_clear_zeros(&pmc->sources);

if (pmc->crcount == 0 && !pmc->tomb && !pmc->sources)
//清除当前组
if (pmc_prev)
pmc_prev->next = pmc_next;
else
in_dev->mc_tomb = pmc_next;
in_dev_put(pmc->interface);
kfree(pmc);
else
pmc_prev = pmc;

//遍历所有活动的组
for (pmc=in_dev->mc_list; pmc; pmc=pmc->next)
//当前组过滤模式为EXCLUDE,则BLOCK为添加、ALLOW为删除
//当前组过滤模式为INCLUDE,则ALLOW为添加、BLOCK为删除
if (pmc->sfcovcunt[MCAST_EXCLUDE])
type = IGMPV3_BLOCK_OLD_SOURCES;
dtype = IGMPV3_ALLOW_NEW_SOURCES;
else
type = IGMPV3_ALLOW_NEW_SOURCES;
dtype = IGMPV3_BLOCK_OLD_SOURCES;

//添加ALLOW/BLOCK过滤源的报告
skb = add_grec(skb, pmc, type, 0, 0);
skb = add_grec(skb, pmc, dtype, 0, 1);

if (pmc->crcount)
//根据IGMPV3标准,当前活动的组为什么过滤模式,就要报告
//TO到对应的模式。
if (pmc->sfmode == MCAST_EXCLUDE)
type = IGMPV3_CHANGE_TO_EXCLUDE;
else
type = IGMPV3_CHANGE_TO_INCLUDE;
skb = add_grec(skb, pmc, type, 0, 0);

pmc->crcount--;

//如果没有生成skb则直接返回,否则将该IGMP报文发出。
if (!skb)
return;
igmpv3_sendpack(skb);

//递减健壮变量,重启动该定时器。这点V3同V1/V2不同,V1/V2在收到网络中其
//它主机对该组已经发送了报告时,就会将重试计数清零。但V3不会因为收到网络中
//其它主机的发送报告,而清零mr_ifc_count的值,不过V3报头格式与V1/V2不同,
//V3的路由器查询报头中含有QRV字段,该字段用于路由器要求主机的健壮变量取
//什么值,也就是这个值由路由器来控制主机。
if (in_dev->mr_ifc_count)
in_dev->mr_ifc_count--;
igmp_ifc_start_timer(in_dev, IGMP_Unsolicited_Report_Interval);

//该定时器启动时增加了in_dev的引用,在定时器超时处理函数中再递减引用计数。
__in_dev_put(in_dev);

---------------------------------------------------------------------------------------------------------------------
//IGMP V1/V2/V3报告定时器超时处理函数
igmp_timer_expire
ip_mc_list *im=(struct ip_mc_list *)data;
in_device *in_dev = im->interface;

im->tm_running=0;		//定时器已经在处理

//当前收到网络内其它主机发送的该多播组的报告,则unsolicit_count将被设置为
//0,否则则重新启动定时器。
if (im->unsolicit_count)
im->unsolicit_count--;
igmp_start_timer(im, IGMP_Unsolicited_Report_Interval);

im->reporter = 1;	//标记已经发送过主机报告

//根据IGMP版本来确定使用使用哪种报告类型
//版本的确定分为三种情况:
//1、用户设置ipv4的全局配置强制使用哪个版本。
//2、用户设置ipv4的指定设备配置强制使用哪个版本。
//3、进行自动协商,依赖路由器发送的组成员查询是哪个版本。
if (IGMP_V1_SEEN(in_dev))
//下面单独分析
igmp_send_report(in_dev, im, IGMP_HOST_MEMBERSHIP_REPORT);
else if (IGMP_V2_SEEN(in_dev))
igmp_send_report(in_dev, im, IGMPV2_HOST_MEMBERSHIP_REPORT);
else
igmp_send_report(in_dev, im, IGMPV3_HOST_MEMBERSHIP_REPORT);

//定时器在启动了,增加了一个引用计数,这里在定时器超时处理函数中去除定时器
//的引用。
ip_ma_put(im);
if (atomic_dec_and_test(&im->refcnt))
in_dev_put(im->interface);
kfree(im);

---------------------------------------------------------------------------------------------------------------------
//V1/V2/V3发送主机报告
igmp_send_report
//IGMP v3版本的报告使用igmpv3_send_report处理,单独分析。
if (type == IGMPV3_HOST_MEMBERSHIP_REPORT)
return igmpv3_send_report(in_dev, pmc);
//离开组的报告发送到224.0.0.2所有路由器组
else if (type == IGMP_HOST_LEAVE_MESSAGE)
dst = IGMP_ALL_ROUTER;
else
dst = group;

struct flowi fl = { .oif = dev->ifindex,.nl_u = { .ip4_u = { .daddr = dst } },
.proto = IPPROTO_IGMP };

//进行路由查找,如果失败则返回
if (ip_route_output_key(&rt, &fl))
return -1;

//路由查找成功,但没有源地址,则返回失败
if (rt->rt_src == 0)
ip_rt_put(rt);
return -1;

skb=alloc_skb(IGMP_SIZE+LL_RESERVED_SPACE(dev), GFP_ATOMIC);
skb->dst = &rt->u.dst;
skb_reserve(skb, LL_RESERVED_SPACE(dev));	//为二层头留空间

//填充IP头
skb->nh.iph = iph = (struct iphdr *)skb_put(skb, sizeof(struct iphdr)+4);
iph->version  = 4;
iph->ihl      = (sizeof(struct iphdr)+4)>>2;	//多出的4个字节是为了增加Alert选项
iph->tos      = 0xc0;
iph->frag_off = htons(IP_DF);	//IGMP报文强制不分片
iph->ttl      = 1;				//IGMP报文只发向局域网
iph->daddr    = dst;
iph->saddr    = rt->rt_src;
iph->protocol = IPPROTO_IGMP;
iph->tot_len  = htons(IGMP_SIZE);
ip_select_ident(iph, &rt->u.dst, NULL);	//IP标识字段

//添加Alert选项,通常组播路由是内核与应用层共同实现的,一般在路由器侧都有
//有一个应用层实现的组播路由程序,该程序会设置MRT_INIT选项,当设置该选
//项后,就会向内核的ip_ra_chain链注册一个条目,内核在组播接收和转发流程都会
//检测ip_ra_chain链是否有在监听的套接口,有的话就把数据直接传给该套接口,这
//样应用层的组播路由程序就可以进行处理。
((u8*)&iph[1])[0] = IPOPT_RA;
((u8*)&iph[1])[1] = 4;
((u8*)&iph[1])[2] = 0;
((u8*)&iph[1])[3] = 0;

//ip校验和
ip_send_check(iph);
//IGMP头处理
ih = (struct igmphdr *)skb_put(skb, sizeof(struct igmphdr));
ih->type=type;
ih->code=0;
ih->csum=0;
ih->group=group;
ih->csum=ip_compute_csum((void *)ih, sizeof(struct igmphdr));

//经过netfileter钩子,假设没有拦截,则使用dst_output将IGMP报告发出,当前
//dst_output可以参考《IP层输出》中的分析。
return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,dst_output);

---------------------------------------------------------------------------------------------------------------------
//V3版本主机报告
igmpv3_send_report
//没有传入指定组播组的参数,则遍历所有组播列表
if (!pmc)
for (pmc=in_dev->mc_list; pmc; pmc=pmc->next)
//224.0.0.1的所有主机组播组不参与报告发送
if (pmc->multiaddr == IGMP_ALL_HOSTS)
continue;

//如果含有EXCLUDE的过滤源,则报告类型为IS_EXCLUDE,否则
//为IS_INCLUDE。
if (pmc->sfcount[MCAST_EXCLUDE])
type = IGMPV3_MODE_IS_EXCLUDE;
else
type = IGMPV3_MODE_IS_INCLUDE;

//生成报告skb,后面单独分析。
skb = add_grec(skb, pmc, type, 0, 0);
//仅处理传入的指定组播组。
else
//如果含有EXCLUDE的过滤源,则报告类型为IS_EXCLUDE,否则
//为IS_INCLUDE。
if (pmc->sfcount[MCAST_EXCLUDE])
type = IGMPV3_MODE_IS_EXCLUDE;
else
type = IGMPV3_MODE_IS_INCLUDE;

//生成报告skb,后面单独分析。
skb = add_grec(skb, pmc, type, 0, 0);

//没有满足发送报文的条件,skb没有生成,则跳过。
if (!skb)
return 0;

//发送IGMPv3报文。
igmpv3_sendpack(skb);
iphdr *pip = skb->nh.iph;
igmphdr *pig = skb->h.igmph;

//填充IP总长度、校验和。
iplen = skb->tail - (unsigned char *)skb->nh.iph;
pip->tot_len = htons(iplen);
ip_send_check(pip);

//计算IGMP校验和
igmplen = skb->tail - (unsigned char *)skb->h.igmph;
pig->csum = ip_compute_csum((void *)skb->h.igmph, igmplen);

//经过netfileter钩子,假设没有拦截,则使用dst_output将IGMP报告发出,当前
//dst_output可以参考《IP层输出》中的分析。
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dev, dst_output);

---------------------------------------------------------------------------------------------------------------------
//生成IGMP v3的IGMP报文skb
//skb为是否传入已分配的skb
//pmc为是否传入指定组播组
//type为要生成的IGMP报文类型
//gdeleted为是否是离开组
//sdeleted为是否要阻塞指定源。
//返回值为待发送的skb。
struct sk_buff *add_grec(struct sk_buff *skb, struct ip_mc_list *pmc,int type, int gdeleted, int sdeleted)
//发到224.0.0,1地址则直接返回
if (pmc->multiaddr == IGMP_ALL_HOSTS)
return skb;

//标记当前是否是普通查询报文的响应
isquery = type == IGMPV3_MODE_IS_INCLUDE ||
type == IGMPV3_MODE_IS_EXCLUDE;

//IMGPV3标准规定,这2种报告类型,如果过滤源太多,则需要截断。
truncate = type == IGMPV3_MODE_IS_EXCLUDE ||
type == IGMPV3_CHANGE_TO_EXCLUDE;

stotal = scount = 0;

//根据传入参数确定过滤源列表,tomb是阻塞的列表,sources是活动的列表
psf_list = sdeleted ? &pmc->tomb : &pmc->sources;

//当前组中没有源列表,则直接进行不含过滤源的报告处理。
if (!*psf_list)
goto empty_source;

//获取IGMP报头
pih = skb ? (struct igmpv3_report *)skb->h.igmph : NULL;

//如果当前报告可以截断类型,则需要检查目前已有的skb是否可以容纳下所需要的
//新的报告项,如果没办法合并了,则需要将之前未发送的IGMP报告立即发送出。
//之后再重新建立新的IGMP报文。
if (truncate)
if (pih && pih->ngrec && 
AVAILABLE(skb) < grec_size(pmc, type, gdeleted, sdeleted))
if (skb)
igmpv3_sendpack(skb);
skb = igmpv3_newpack(dev, dev->mtu);

first = 1;
psf_prev = NULL;

//遍历所有过滤源
for (psf=*psf_list; psf; psf=psf_next)
psf_next = psf->sf_next;

//检测当前过滤源是否需要加入到报告报文中。
if (!is_in(pmc, psf, type, gdeleted, sdeleted))
psf_prev = psf;
continue;

//当前报告是普通查询时,清楚sf_gsresp,这样在没有收到应答之前,如果出现再
//次查询,则不会再次处理。
if (isquery)
psf->sf_gsresp = 0;

//如果当前skb剩余空间不能容纳新的报告,则在不截断的情况下,立即发送未
//处理的报文,之后生成新的skb。
if (AVAILABLE(skb) < sizeof(__be32) +first*sizeof(struct igmpv3_grec))
if (truncate && !first)
break;
if (pgr)
pgr->grec_nsrcs = htons(scount);
if (skb)
igmpv3_sendpack(skb);
skb = igmpv3_newpack(dev, dev->mtu);
first = 1;
scount = 0;

//当前是新的skb报文,则需要添加组记录头
if (first)
skb = add_grhead(skb, pmc, type, &pgr);
first = 0;

//向报告中添加过滤源地址
psrc = (__be32 *)skb_put(skb, sizeof(__be32));
*psrc = psf->sf_inaddr;
scount++; stotal++;

//如果当前报告类型为ALLOW_NEW_SOURCES或者BLOCK_OLD_SOURCES
//则递减报文改变计数,到达0后,如果进行删除处理。
if ((type == IGMPV3_ALLOW_NEW_SOURCES ||
type == IGMPV3_BLOCK_OLD_SOURCES)&& psf->sf_crcount)
psf->sf_crcount--;
if ((sdeleted || gdeleted) && psf->sf_crcount == 0)
if (psf_prev)
psf_prev->sf_next = psf->sf_next;
else
*psf_list = psf->sf_next;
kfree(psf);
continue;

psf_prev = psf;

empty_source:

//当前组没有过滤源
if (!stotal)
//合法性类型检测
if (type == IGMPV3_ALLOW_NEW_SOURCES ||
type == IGMPV3_BLOCK_OLD_SOURCES)
return skb;

//设置报告头,如果当前skb已经不能容纳,则将未发送的IGMP报文立即发
//送出去,并重新分配skb。
if (pmc->crcount || isquery)
if (skb && AVAILABLE(skb)grec_nsrcs = htons(scount);

//当前普通查询的报告,则清除指定源请求的标记。
if (isquery)
pmc->gsquery = 0;

//返回构建好的IGMP报文。
return skb;

二、IGMP路由器处理报告
组播的报告处理不同其它网络功能模块,通常是和内核侧程序与用户侧程序一起完成,通常在用户侧有一个多播路由进程,由该进程负责IGMP报告的处理,以及组播路由的维护。当前因工作关系,分析了braodcom家庭网关方案的多播路由进程实现。大体包含如下流程(不涉及IGMP SNOOPING、IGMP PROXY):
1、多播路由进程启动后,使用MRT_INIT进行初始化
//向ip_ra_chain路由告警链表中添加一项,之后当收到含有ROUTE_ALERT选项的报文
//收直接通过该用户侧套接口进行接收处理。
ip_ra_control(sk, 1, mrtsock_destruct);
rap = &ip_ra_chain;
ra=*rap;

new_ra->sk = sk;
new_ra->destructor = destructor;	//mrtsock_destruct
new_ra->next = ra;
*rap = new_ra;
sock_hold(sk);

//设置当前组播套接口,当收到组播报文,但没有对应的组播路由时,可以通过该套
//接口将收到的报文转给应用层的组播路由进程处理。
mroute_socket=sk;

//设置设备配置信息,开启组播路由转发。
ipv4_devconf.mc_forwarding++;

2、根据配置文件使用MRT_ADD_VIF创建组播虚拟接口
vif_add(&vif, sk==mroute_socket);
switch (vifc->vifc_flags)
//假设当前添加的虚拟接口类型为默认的非隧道方式
case 0:
//获取关联的真实接口设备
dev = ip_dev_find(vifc->vifc_lcl_addr.s_addr);
dev_put(dev);

//标记当前设备开启了多播路由转发
in_dev = __in_dev_get_rtnl(dev)
in_dev->cnf.mc_forwarding++;

//将真实设备设置允许所有多播报文的标记,同时调用设备虚拟的接口进行设置
dev_set_allmulti(dev, +1);
old_flags = dev->flags;
dev->flags |= IFF_ALLMULTI;
if (dev->flags ^ old_flags)
dev_mc_upload(dev);

//进行路由缓冲刷新
ip_rt_multicast_event(in_dev);

//将新建的虚拟接口添加到vif_table中
vifi = vifc->vifc_vifi;
v = &vif_table[vifi];

v->rate_limit=vifc->vifc_rate_limit;
v->local=vifc->vifc_lcl_addr.s_addr;
v->remote=vifc->vifc_rmt_addr.s_addr;
v->flags=vifc->vifc_flags;
if (!mrtsock)
v->flags |= VIFF_STATIC;	//非应用层多播路由进程创建则为静态
v->threshold=vifc->vifc_threshold;
v->bytes_in = 0;
v->bytes_out = 0;
v->pkt_in = 0;
v->pkt_out = 0;
v->link = dev->ifindex;	//关联的真实设备接口索引
dev_hold(dev);
v->dev=dev;
if (vifi+1 > maxvif)
maxvif = vifi+1;

3、当收到主机的IGMP组报告后,使用MRT_ADD_MFC添加多播路由。
//用户侧传入如下4个参数,根据过滤源地址和组地址构成组播路由关键条目项,
//之后从上行虚拟接口进来的匹配组播报文,都转发到下行虚拟接口。
//mfc->mfcc_origin.s_addr		过滤源地址
//mfc->mfcc_mcastgrp.s_addr	组地址
//mfc->mfcc_parent			上行虚拟接口id
//mfc->mfcc_ttls				所有下行虚拟接口id
ipmr_mfc_add(&mfc, sk==mroute_socket)
//根据过滤源地址和组地址生成hahs key
line=MFC_HASH(mfc->mfcc_mcastgrp.s_addr, mfc->mfcc_origin.s_addr);

c->mfc_origin=mfc->mfcc_origin.s_addr;
c->mfc_mcastgrp=mfc->mfcc_mcastgrp.s_addr;
c->mfc_parent=mfc->mfcc_parent;

//添加所有下行虚拟接口索引
//cache->mfc_un.res.ttls[vifi] = ttls[vifi]
ipmr_update_thresholds(c, mfc->mfcc_ttls);

//判断当前是否是用户侧组播路由进程的套接口,如果非用户侧进程添加,则
//标记该路由项为静态。
if (!mrtsock)
c->mfc_flags |= MFC_STATIC;

//将该项加入到组播路由缓存组中
c->next = mfc_cache_array[line];
mfc_cache_array[line] = c;

//检测之前是否含有未处理的多播报文,如果之前从上行收到多播报文,但没有对应的
//多播路由时,就会将该报文存储,同时通知用户侧组播路由进行多播路由创建。
for (cp = &mfc_unres_queue; (uc=*cp) != NULL;cp = &uc->next)
if (uc->mfc_origin == c->mfc_origin &&uc->mfc_mcastgrp == c->mfc_mcastgrp)
*cp = uc->next;
if (atomic_dec_and_test(&cache_resolve_queue_len))
del_timer(&ipmr_expire_timer);
break;

//如果之前有未处理的多播报文
if (uc)
//处理之前的多播报文
ipmr_cache_resolve(uc, c);
//从unresolved队列中取出一个阻塞的多播报文
while((skb=__skb_dequeue(&uc->mfc_un.unres.unresolved)))
//当前如果skb已经是netlink消息报文则进行netlink相关处理
if (skb->nh.iph->version == 0)
//将当前多播路由上行接口、下行接口信息设置到netlink消息中
//,并发送给正在监听该netlink报文的进程。
if (ipmr_fill_mroute(skb, c, NLMSG_DATA(nlh)) > 0)
nlh->nlmsg_len = skb->tail - (u8*)nlh;
else
nlh->nlmsg_type = NLMSG_ERROR;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
skb_trim(skb, nlh->nlmsg_len);
e = NLMSG_DATA(nlh);
e->error = -EMSGSIZE;
memset(&e->msg, 0, sizeof(e->msg));
rtnl_unicast(skb, NETLINK_CB(skb).pid);
//否则当前skb是原始未处理报文,则进行多播报文转发
else
//在下面转发路由小节分析。
ip_mr_forward(skb, c, 0);

kmem_cache_free(mrt_cachep, uc);

三、IGMP路由器发送组查询
通常是用户侧的组播路由进程来完成,创建一个原始套接口,定期发送组查询IGMP报文。

四、IGMP主机响应查询
当一个接口设备创建后,都会自动加入到224.0.0.1的所有主机多播组中,这时候如果收到网络侧发来的多播组查询,就会走到本地接收流程,在内核侧注册了IGMP类型报文处理,这时候就会调用注册的处理回调igmp_rcv进行多播组查询处理。

igmp_rcv
//检测当前skb报文长度是否有效
If(!pskb_may_pull(skb, sizeof(struct igmphdr)))
goto drop;

//校验和处理,如果硬件完成,检测是否有效,如果无效,或硬件未完成,则进行软件
//校验和处理。
switch (skb->ip_summed)
case CHECKSUM_COMPLETE:
if (!csum_fold(skb->csum))
break;
case CHECKSUM_NONE:
skb->csum = 0;
if (__skb_checksum_complete(skb))
goto drop;

ih = skb->h.igmph;
switch (ih->type)
case IGMP_HOST_MEMBERSHIP_QUERY:
igmp_heard_query(in_dev, skb, len);
//V1/V2版本的查询
if (len == 8)
//V1版本的查询
if (ih->code == 0)
//设置一个默认的延时响应时间
max_delay = IGMP_Query_Response_Interval;

//设置v1版本的发现时间,主要用于版本兼容处理,如果当前收到
//v1版本的查询则在这段时间内,都按v1版本的标准发送组报告。
//否则当时间超时后,没有再次收到v1版本的查询,则看是否有
//v2版本的查询存在,依次类推,v1/v2版本查询都不存在时,则
//默认按v3版本的查询来处理。
in_dev->mr_v1_seen = jiffies + IGMP_V1_Router_Present_Timeout;

group = 0;
//V2版本的查询
else
//从IGMP查询报告头获取最大响应时间,单位是0.1秒
max_delay = ih->code*(HZ/IGMP_TIMER_SCALE);

//设置v2版本的发现时间
in_dev->mr_v2_seen = jiffies + IGMP_V2_Router_Present_Timeout;

//如果之前有v3版本接口改变触发的报告延时定时器,则停止。
in_dev->mr_ifc_count = 0;
if (del_timer(&in_dev->mr_ifc_timer))
__in_dev_put(in_dev);

//如果之前有v3版本导致的待删除的组列表、过滤源列表,则当前立
//即删除。
igmpv3_clear_delrec(in_dev);
//非法报文
else if (len < 12)
return
//V3版本的查询
else
//合法性校验
if (!pskb_may_pull(skb, sizeof(struct igmpv3_query)))
return;

//如果当前查询包含指定源,则校验源列表长度是否合法
ih3 = (struct igmpv3_query *) skb->h.raw;
if (ih3->nsrcs)
if (!pskb_may_pull(skb, sizeof(struct igmpv3_query)
+ ntohs(ih3->nsrcs)*sizeof(__be32)))
return;
ih3 = (struct igmpv3_query *) skb->h.raw;

//从IGMP查询报告头获取最大响应时间,单位是0.1秒
max_delay = IGMPV3_MRC(ih3->code)*(HZ/IGMP_TIMER_SCALE);
if (!max_delay)
max_delay = 1;

in_dev->mr_maxdelay = max_delay;

//从查询报告头中获取健壮值
if (ih3->qrv)
in_dev->mr_qrv = ih3->qrv;

//如果查询报文指定的组地址为0,则启用V3版本特有的普通查询定时
//器,在该定时器超时后,进行V3版本的普通查询报告。
if (!group)
if (ih3->nsrcs)
return;
igmp_gq_start_timer(in_dev);
tv = net_random() % in_dev->mr_maxdelay;
in_dev->mr_gq_running = 1;
mod_timer(&in_dev->mr_gq_timer, jiffies+tv+2)
in_dev_hold(in_dev);
return;

//标明当前查询含源
mark = ih3->nsrcs != 0;

//遍历当前接口所有多播组
for (im=in_dev->mc_list; im!=NULL; im=im->next)
//如果指定了查询的组,则过滤不匹配的多播组
if (group && group != im->multiaddr)
continue;

//忽略224.0.0.1多播地址的查询,因为不管主机还是路由器,只要
//接口启动,内核默认都将接口加入了224.0.0.1的组,所以不需要
//查询。
if (im->multiaddr == IGMP_ALL_HOSTS)
continue;

//设置当前是否是含源查询
//1、如果还有未完成的报告,则旧的必须是含源查询,新的也必须是
//含源查询,则才能设置gsquery为true,gsquery主要用于下面的重
//新构建源列表及触发新的报告定时器。如果旧的与新的有任何一个是
//不含源的查询,则不需要重新构建源列表,并且必须触发报告定时器
//2、如果之前没有未完成的报告,则根据当前是否含源来设置gsquery
//的值。
if (im->tm_running)
im->gsquery = im->gsquery && mark;
else
im->gsquery = mark;

//1、如果当前是不含源的查询,则必须触发新的查询定时器。
//2、如果当前是含源的查询,则检测如果之前是不含源的查询,则必须
//触发新的查询定时器,否则当前和之前都是含源的查询,则需要进行
//源列表处理,如果新的源查询在当前源列表中找不到匹配的源,则不重
//新启动定时器,使用以前的定时器。
changed = !im->gsquery || 
igmp_marksources(im, ntohs(ih3->nsrcs), ih3->srcs);

//启用新的定时器
if (changed)
igmp_mod_timer(im, max_delay);
//如果收到报告查询,则清除新加入组时主动报告的触发
im->unsolicit_count = 0;

//如果之前的定时器时间比较短,则使用之前的定时器时间
//否则使用新的定时器时间。定时器超时后发送组报告。
if (del_timer(&im->timer))
if ((long)(im->timer.expires-jiffies) < max_delay)
add_timer(&im->timer);
im->tm_running=1;
return;
igmp_start_timer(im, max_delay);

case IGMP_HOST_MEMBERSHIP_REPORT:
case IGMPV2_HOST_MEMBERSHIP_REPORT:
case IGMPV3_HOST_MEMBERSHIP_REPORT:
//注释上写回环情况
if (((struct rtable*)skb->dst)->fl.iif == 0)
break;

//和当前小节描述关系不大,主要作用是当本地主机准备发送组播报告时,是有一
//个延时时间,在这个时间内如果收到网络内其它主机相同组的报告,则本地主机
//取消延时定时器,不再向网络上的路由设备发送组播报告,因为有其它主机已经
//发送过了。
if (skb->pkt_type == PACKET_MULTICAST 
||skb->pkt_type == PACKET_BROADCAST)
igmp_heard_report(in_dev, ih->group);
for (im=in_dev->mc_list; im!=NULL; im=im->next)
if (im->multiaddr == group)
igmp_stop_timer(im);
break;

五、接收组播报文
接收组播报文涉及路由模块的相关处理,路由模块的细节以后在分析,当前仅关注组播数据的关键处理流程。
当组播报文从网卡接收后,如果三层报文类型为ipv4,则使用ip_rcv进行处理,经过netfilter的PRE_ROUTING链后,继续调用ip_rcv_finish进行处理,在ip_rcv_finish中,首先使用ip_route_input进行选路处理,注意接收组播报文的选路处理比较特殊,它并不是使用单播常用的路由表进行选路处理,如果本地接口有加入该组播,则往本地传送一份报文,之后根据组播专门的组播路由表进行往其它外部接口进行转发,通常组播路由表是由用户侧的组播路由进程来维护组播路由表。在ip_route_input中如果查找路由缓存失败,并判断出当前目的地址是组播地址,进行组播处理。如果以下两种条件任意满足,则进行处理,否则丢弃报文。
1、检测本地加入的组播地址及过滤源是否与组播报文匹配,如果匹配则继续处理。
2、如果情况1不匹配,则表明本地并没有加入该组播,查看当前组播地址是否是非本地组播类型(即非224.0.0.X的地址),并且当前设备开启了组播转发标记(通常用户侧多播路由调用MRT_INIT套接口选项后,该标记就是递增),则表示需要进行组播路由处理,这时候可以继续进行组播处理。

之后ip_route_input的关键点就是对rth->u.dst.input回调的设置。
1、如果本地加入了该多播组,则设置该回调为ip_local_deliver,该回调仅将报文向本地上层传递。
2、如果当前组播地址是非本地组播类型(即非224.0.0.X的地址),并且开启了组播转发,则将回调替换为ip_mr_input,该回调除了负责将报文向本地上层传递(如果本地加入了该组),还会进行组播路由的转发。

当前仅关注ip_mr_input的分析。
ip_mr_input
//该标记是在ip_route_input中设置的,表明本地加入了该多播组,需要将报文向
//本地上层传递一份。
local = ((struct rtable*)skb->dst)->rt_flags&RTCF_LOCAL;

//保护处理,如果当前组播报文已经通过转发处理发送到其它外部接口,当前又从
//环回接口接收上来,如果判断已经转发过,则不再进行转发处理,仅检查本地是
//否加入组,如果加入仅进行本地处理。
if (IPCB(skb)->flags&IPSKB_FORWARDED)
goto dont_forward;

//如果本地没有加入该多播组
if (!local)
//如果当前报文含有IP的ROUTE_ALERT选项,则通常是IGMP报文,这时候检
//当前ip_ra_chain链表是否有条目,如果有则交给该链表条目上的设置的套接口
//进行处理,通常应用层的多播路由进程在初始化时会调用MRT_INIT套接口选
//项,内核侧会将该应用层的套接口条目添加到ip_ra_chain链表,同时也会设置
//全局变量mroute_socket指向该套接口。
if (IPCB(skb)->opt.router_alert)
//将该报文交给应用层的进程来处理。
if (ip_call_ra_chain(skb))
return 0;
//有些厂商设备的IGMP报文不标准,没有协带IP的ROUTE_ALERT选项,这时
//候就检测是否有mroute_socket应用层的套接口,如果有则将该报文交给应用层的
//进程来处理。
else if (skb->nh.iph->protocol == IPPROTO_IGMP)
if (mroute_socket)
nf_reset(skb);
raw_rcv(mroute_socket, skb);
return 0;

//在mfc_cache_array多播路由表里查找是否有匹配的多播地址及过滤源。通常多播路由
//表是由用户侧的多播路由进程来维护。
cache = ipmr_cache_find(skb->nh.iph->saddr, skb->nh.iph->daddr);

//在多播路由表中没有找到
if (cache==NULL)
//当前本地已经加入该多播组,则复制一份报文上传到本地。
if (local)
skb2 = skb_clone(skb, GFP_ATOMIC);
ip_local_deliver(skb);
skb = skb2;

//查找用户侧是否创建的多播的虚拟接口
vif = ipmr_find_vif(skb->dev);

//当前虽然没有多播路由,但是用户侧进程创建了虚拟接口,进行尝试处理
if (vif >= 0)

ipmr_cache_unresolved(vif, skb);
//在未处理列表中查找是否已经有正在处理该多播组地址
for (c=mfc_unres_queue; c; c=c->next)
if (c->mfc_mcastgrp == skb->nh.iph->daddr &&
c->mfc_origin == skb->nh.iph->saddr)
break;

if (c == NULL)
//当前没有该多播组地址的未处理项,在未到达上限时,则分
//配一个新的未处理项的资源块。
if (atomic_read(&cache_resolve_queue_len)>=10 ||
(c=ipmr_cache_alloc_unres())==NULL)
kfree_skb(skb);
return -ENOBUFS;

//初始化
c->mfc_parent=-1;
c->mfc_origin=skb->nh.iph->saddr;
c->mfc_mcastgrp=skb->nh.iph->daddr;

//构建一个skb报文,发送给应用层的多播路由进程,通知其收到了
//一个多播报文,但没有匹配到对应的多播路由。
ipmr_cache_report(skb, vifi, IGMPMSG_NOCACHE)
skb = alloc_skb(128, GFP_ATOMIC);

skb->nh.iph = (struct iphdr *)skb_put(skb, ihl);
memcpy(skb->data,pkt->data,ihl);
skb->nh.iph->protocol = 0;
msg = (struct igmpmsg*)skb->nh.iph;
msg->im_vif = vifi;
skb->dst = dst_clone(pkt->dst);

igmp=(struct igmphdr *)skb_put(skb,sizeof(struct igmphdr));
igmp->type	=
msg->im_msgtype = assert;
igmp->code 	=	0;
skb->nh.iph->tot_len=htons(skb->len);
skb->h.raw = skb->nh.raw;

sock_queue_rcv_skb(mroute_socket,skb)
return ret;

//递增当前已经未处理的多播地址个数,同时将新建的未处理条目
//添加到mfc_unres_queue列表中。
atomic_inc(&cache_resolve_queue_len);
c->next = mfc_unres_queue;
mfc_unres_queue = c;

//启动一个10秒的定时器,超时后检测如果当前多播地址还是未处
//理,则从mfc_unres_queue列表清除。
mod_timer(&ipmr_expire_timer, c->mfc_un.unres.expires);

//一个未处理的多播组仅能暂存3个报文,超过则错误。
if (c->mfc_un.unres.unresolved.qlen>3)
kfree_skb(skb);
err = -ENOBUFS;
else
skb_queue_tail(&c->mfc_un.unres.unresolved,skb);
err = 0;

return err;

return err;

//当前多播路由没有对应项,同时也没有创建多播的虚拟接口,则报文
//直接丢弃。
kfree_skb(skb);
return -ENODEV;

//当前找到了多播路由,则进行转发处理。下面单独分析该函数。
ip_mr_forward(skb, cache, local);

//如果本地已经加入该多播组,则将报文向本地上层传递。
if (local)
return ip_local_deliver(skb);

return 0;

//当前为不再进行转发组播的情况,此时仅判断是否进行本地上层传递的处理。否则直接
//将报文丢弃。
dont_forward:
if (local)
return ip_local_deliver(skb);
kfree_skb(skb);
return 0;

六、转发组播报文
ip_mr_forward
//期望发送方的虚拟接口
vif = cache->mfc_parent;
cache->mfc_un.res.pkt++;
cache->mfc_un.res.bytes += skb->len;

//当前接收多播的接口与期望的接口不同
if (vif_table[vif].dev != skb->dev)
//如果路由查找后的源接口为0,则为回环接口,则不能进行转发处理。
if (((struct rtable*)skb->dst)->fl.iif == 0)
goto dont_forward;

cache->mfc_un.res.wrong_if++;
//查找当前发送方的正确虚拟接口
true_vifi = ipmr_find_vif(skb->dev);

//如果找到了正确的虚拟接口,当前设置PIM assert,当有收到PIM协议,或者
//当前虚拟接口的TTL值有效,发送错误接口类型的报告。当有仅对IGMP进行
//了了解,对PIM域间路由协议不太清楚。
if (true_vifi >= 0 && mroute_do_assert &&(mroute_do_pim || 
cache->mfc_un.res.ttls[true_vifi] < 255) &&time_after(jiffies,
cache->mfc_un.res.last_assert + MFC_ASSERT_THRESH))
cache->mfc_un.res.last_assert = jiffies;
ipmr_cache_report(skb, true_vifi, IGMPMSG_WRONGVIF);

//接收接口与期望接口不匹配,不进行转发。
goto dont_forward;

vif_table[vif].pkt_in++;
vif_table[vif].bytes_in+=skb->len;

//遍历当前多播路由项对应的所有转发目地虚拟接口
for (ct = cache->mfc_un.res.maxvif-1; ct >= cache->mfc_un.res.minvif; ct--)
//仅转发符合TTL需求的虚拟接口
if (skb->nh.iph->ttl > cache->mfc_un.res.ttls[ct])
if (psend != -1)
//将报文进行复制,之后向当前虚拟接口发送。
skb2 = skb_clone(skb, GFP_ATOMIC);
ipmr_queue_xmit(skb2, cache, psend);
psend=ct;

if (psend != -1)
if (local)
skb2 = skb_clone(skb, GFP_ATOMIC);
ipmr_queue_xmit(skb2, cache, psend);
else
ipmr_queue_xmit(skb, cache, psend);
return 0;

dont_forward:
if (!local)
kfree_skb(skb);

-------------------------------------------------------------------------------------------------------------------
ipmr_queue_xmit
//虚拟接口没有关联设备,丢弃
if (vif->dev == NULL)
goto out_free;

//如果当前是用于PIM协议创建的VIFF_REGISTER类型接口,则将报文直接传到
//应用层的多播路由进程。
if (vif->flags & VIFF_REGISTER)
vif->pkt_out++;
vif->bytes_out+=skb->len;
netdev_priv(vif->dev))->tx_bytes += skb->len;
netdev_priv(vif->dev))->tx_packets++;
ipmr_cache_report(skb, vifi, IGMPMSG_WHOLEPKT);
kfree_skb(skb);
return;

//当前虚拟接口类型是隧道类型
if (vif->flags&VIFF_TUNNEL)
struct flowi fl = { .oif = vif->link,
.nl_u = { .ip4_u =
{ .daddr = vif->remote,
.saddr = vif->local,
.tos = RT_TOS(iph->tos) } },
.proto = IPPROTO_IPIP };

//根据虚拟接口设置的远端地址进行路由查询
ip_route_output_key(&rt, &fl)

//额外封装头的大小
encap = sizeof(struct iphdr);
//普通虚拟接口类型
else
struct flowi fl = { .oif = vif->link,
.nl_u = { .ip4_u =
{ .daddr = iph->daddr,
.tos = RT_TOS(iph->tos) } },
.proto = IPPROTO_IPIP };

//根据多播的目的地址及输出物理接口进行路由查询,如果路由查询成功,则设置
// rt->u.dst.output为ip_mc_output,路由模块细节以后在分析。

ip_route_output_key(&rt, &fl)

dev = rt->u.dst.dev;
//如果组播报文最终大于超出路径MTU,同时当前报文禁止分片,则丢弃。
if (skb->len+encap > dst_mtu(&rt->u.dst) && (ntohs(iph->frag_off) & IP_DF))
IP_INC_STATS_BH(IPSTATS_MIB_FRAGFAILS);
ip_rt_put(rt);
goto out_free;

//封装头的长度
encap += LL_RESERVED_SPACE(dev) + rt->u.dst.header_len;

//对于隧道类型的接口,如果封装头的大小超出skb头部预留的空间,则需要
//将skb重新处理。如果处理失败,比如内存不够了,则丢弃。
if (skb_cow(skb, encap))
ip_rt_put(rt);
goto out_free;

vif->pkt_out++;
vif->bytes_out+=skb->len;

dst_release(skb->dst);
skb->dst = &rt->u.dst;

//递减TTL头字段
iph = skb->nh.iph;
ip_decrease_ttl(iph);

//如果当前是隧道类型接口,则进行封装头的填充,构建一个IP头,其它源地址为本地
//地址,目标地址为隧道远端地址,IP负载类型为IPPROTO_IPIP
if (vif->flags & VIFF_TUNNEL)
ip_encap(skb, vif->local, vif->remote);
((struct ip_tunnel *)netdev_priv(vif->dev))->stat.tx_packets++;
((struct ip_tunnel *)netdev_priv(vif->dev))->stat.tx_bytes+=skb->len;

//标记当前skb已经转发处理了。
IPCB(skb)->flags |= IPSKB_FORWARDED;

//当前假设IP_FORWARD链没有任何限制规则,则调用回调ipmr_forward_finish
NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, dev,ipmr_forward_finish);

return;

out_free:
kfree_skb(skb);
return;
-------------------------------------------------------------------------------------------------------------------
ipmr_forward_finish
IP_INC_STATS_BH(IPSTATS_MIB_OUTFORWDATAGRAMS);

//如果IP头含有转发相关选项,则进行IP选项处理。
if (unlikely(opt->optlen))
ip_forward_options(skb);

//调用skb->dst->output回调输出报文。当前假设多播虚拟接口为普通类型,则回调
//函数为ip_mc_output。该函数在下面发送组播报文小节单独分析。
return dst_output(skb);

七、发送组播报文
//当本地输出多播时,或者转发时,在路由查找处理成功后,都会调用该函数进行报文输出。
ip_mc_output
struct sock *sk = skb->sk;
struct rtable *rt = (struct rtable*)skb->dst;
struct net_device *dev = rt->u.dst.dev;

IP_INC_STATS(IPSTATS_MIB_OUTREQUESTS);

skb->dev = dev;	//输出设备
skb->protocol = htons(ETH_P_IP);

//当前是多播报文
if (rt->rt_flags&RTCF_MULTICAST)
//a、如果当前报文是转发类型,当本地加入该多播组时,或者之前还没转发过,则
//复制一份到本地回环接口。
//b、如果当前报文是本地输出,同时本地套接口设置了组播环回处理,当本地加
//入该多播组时,或者之前还没转发过,则复制一份到本地回环接口。
if ((!sk || inet_sk(sk)->mc_loop)
&& ((rt->rt_flags&RTCF_LOCAL) || !(IPCB(skb)->flags&IPSKB_FORWARDED))
newskb = skb_clone(skb, GFP_ATOMIC);
NF_HOOK(PF_INET, NF_IP_POST_ROUTING, newskb, NULL,newskb->dev,
ip_dev_loopback_xmit);

//如果报文的TTL已经减为0,则丢弃。
if (skb->nh.iph->ttl == 0)
kfree_skb(skb);
return 0;

//如果是广播类型,则向本地回环接口发送一份。
if (rt->rt_flags&RTCF_BROADCAST)
newskb = skb_clone(skb, GFP_ATOMIC);
NF_HOOK(PF_INET, NF_IP_POST_ROUTING, newskb, NULL,
newskb->dev, ip_dev_loopback_xmit);

//经过POST_ROUTING链处理后,调用回调ip_finish_output将报文发出。这里之后
//同单播输出相同,不再进行分析。
NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb, NULL, skb->dev,
ip_finish_output, !(IPCB(skb)->flags & IPSKB_REROUTED));

你可能感兴趣的:(IP组播)