跳到主要内容

8.7 接收 IPv6 组播数据包

上一节我们追踪了单播包在内核里的漂流路线,不管是本地投递还是转发,最终归宿都很明确。但网络世界里还有一种「一人说话,万人聆听」的通信模式——组播。对于路由器来说,组播包的处理逻辑显然要更复杂一点:它不仅要决定自己是否要听,还要决定是不是要帮邻居们转发。

这就引出了我们要讨论的 ip6_mc_input()

组播包的岔路口

还记得我们在 ipv6_rcv() 里提到的 ip6_rcv_finish() 吗?它在调用 ip6_route_input() 查路由时,如果发现目标地址是组播地址,就会把接收回调设置成 ip6_mc_input,而不是普通的 ip6_input

这就像是在邮政分拣系统里专门拉了一条「特快专递」通道。让我们来看看这条通道是怎么运作的。

ip6_mc_input() 的任务很直观:拿着包里的目的地址,去问问网卡「你订阅了这个频道吗?」。它通过 ipv6_chk_mcast_addr() 来做这件事:

deliver = ipv6_chk_mcast_addr(skb->dev, &hdr->daddr, NULL);

注意这行代码第三个参数传的是 NULL。这很关键——它意味着我们只关心「有没有订阅这个组播地址」,暂时不关心「是哪个人发过来的」(源过滤)。源过滤是个更精细的活儿,我们后面会专门讲。

现在,deliver 变量掌握着这个包的命运:如果是 true,说明本机对这个组播流量感兴趣;如果是 false,这个包对我们就是垃圾。

路由器的责任:转发还是丢弃?

如果这个内核编译时开启了 CONFIG_IPV6_MROUTE(组播路由支持),事情就变得更有趣了。这时候这台机器不仅仅是个听众,它还是个「中继站」。

代码里有一段预处理逻辑:

#ifdef CONFIG_IPV6_MROUTE
if (dev_net(skb->dev)->ipv6.devconf_all->mc_forwarding &&
!(ipv6_addr_type(&hdr->daddr) &
(IPV6_ADDR_LOOPBACK|IPV6_ADDR_LINKLOCAL)) &&
likely(!(IP6CB(skb)->flags & IP6SKB_FORWARDED))) {
/*
* Okay, we try to forward - split and duplicate
* packets.
*/
struct sk_buff *skb2;

if (deliver)
skb2 = skb_clone(skb, GFP_ATOMIC);
else {
skb2 = skb;
skb = NULL;
}

if (skb2) {
ip6_mr_input(skb2);
}
}
#endif

这段逻辑有点绕,我们拆开来看:

  1. 前提检查:只有全局配置开启了 mc_forwarding,且目标地址不是 loopbacklinklocal 时,才考虑转发。
  2. 克隆还是挪用
    • 如果 deliver 为真(本机也要听),内核就调用 skb_clone() 复制一份 skb2 出来。原来的 skb 留给自己处理,克隆出来的 skb2 拿去转发。这很合理,一人一份,互不干扰。
    • 如果 deliver 为假(本机不感兴趣),那就不用复制了,直接把 skb 挪给转发逻辑(skb2 = skb),并把原来的指针置空(skb = NULL)。这能省一点 CPU 和内存,毕竟没人要的东西就没必要拷贝。

最后,克隆或挪出来的 skb2 被丢进了 ip6_mr_input()。这会进入 IPv6 组播路由子系统(位于 net/ipv6/ip6mr.c)。这个子系统的实现和 IPv4 的组播路由几乎如出一辙(我们在第 6 章聊过),这里就不展开了。这套机制是在 2008 年的内核 2.6.26 里加入的,源自 Mickael Hoerdt 的补丁。

本地投递与源过滤的再次确认

转发逻辑处理完后(如果没开启转发,这步直接跳过),剩下的 skb 要么是自己留用的,要么是没人要的。

if (likely(deliver))
ip6_input(skb);
else {
/* discard */
kfree_skb(skb);
}

return 0;
}

如果 deliver 为真,流程就会走到我们熟悉的 ip6_input(),进而调用 ip6_input_finish()

但这里有个容易让人晕车的细节。

前面在 ip6_mc_input() 里,我们调用了一次 ipv6_chk_mcast_addr(skb->dev, &hdr->daddr, NULL)。 现在到了 ip6_input_finish() 里,代码还会再调用一次 ipv6_chk_mcast_addr(),而且这次第三个参数不是 NULL,而是 hdr->saddr(源地址)。

为什么要查两遍?

第一次查是「粗筛」,只看有没有订阅这个组。第二次查是「精筛」,不仅要看组,还要结合源地址看有没有过滤规则(Source Filtering)。这在下一节「Multicast Source Filtering (MSF)」里会详细讲。你可以把它想象成:

  1. 第一关:你是不是这个俱乐部的会员?
  2. 第二关:你是不是屏蔽了这个俱乐部里某个特定的人发言?

只有过了这两关,数据包才会被递交到上层的 Socket(比如 TCP 或 UDP)。如果没过,ip6_input_finish() 就会把它扔掉。

小结

IPv6 组播接收路径的设计体现了内核处理分组的典型思路:

  1. 早期分流:通过路由子系统 (ip6_route_input) 将组播包引导到 ip6_mc_input
  2. 身份检查:先快速检查本机是否订阅了该组播地址。
  3. 转发与接收的分离:通过 skb_clone 巧妙地在「需要本地接收」和「需要转发」之间复用数据包,避免不必要的内存拷贝。
  4. 分层过滤:先在设备层做粗略的组播地址检查,最后在上层投递前再做带源地址的精细过滤。

这套流程确保了组播数据既能高效地在路由器间转发,又能精准地只到达那些真正感兴趣且符合过滤条件的接收进程。接下来,我们将深入那个「精筛」环节——MLD 协议与源过滤机制。