ch06_5
6.5 The ip_mr_forward() Method
在上一节的结尾,我们提到 ip_mr_input() 做完了一切复杂的查表、缓存未命中处理和通知守护进程的脏活累活之后,会把接力棒交给 ip_mr_forward()。如果说 ip_mr_input 是负责调度和决策的“大脑”,那么 ip_mr_forward 就是负责干脏活累活的“肌肉”——它的任务只有一个:根据 MFC 缓存里的指示,把这个数据包复制并转发到所有该去的虚拟接口(VIF)上。
这个过程听起来简单,但实际操作时有一堆坑:TTL 检查、接口验证、(*,G) 与 的特殊处理,甚至还要处理路由器发出的包又绕回来的尴尬情况。
让我们打开这个函数的引擎盖,看看它是运作的。
初始检查与统计更新
函数一上来,先拿出了两个关键变量:true_vifi 和 vif。
static int ip_mr_forward(struct net *net, struct mr_table *mrt,
struct sk_buff *skb, struct mfc_cache *cache,
int local)
{
int psend = -1;
int vif, ct;
int true_vifi = ipmr_find_vif(mrt, skb->dev);
vif = cache->mfc_parent;
这里有一个微妙但重要的区别:
true_vifi:代表数据包实际上是从哪个物理接口进来的。它是通过拿着包的skb->dev去查 VIF 表得到的。vif:代表MFC 缓存里记录的这个条目的父接口(mfc_parent)。也就是说,路由条目认为这个包“应该”从哪进来。
这两个值如果不一致,后面就会触发一系列的检查。在这之前,内核先做了一波顺手的人情——更新统计信息:
cache->mfc_un.res.pkt++;
cache->mfc_un.res.bytes += skb->len;
处理 (*, G) 的特殊情况
紧接着,代码针对 (*, G) 类型的组播路由做了一个特殊的前置检查。
if (cache->mfc_origin == htonl(INADDR_ANY) && true_vifi >= 0) {
struct mfc_cache *cache_proxy;
/* For an (*,G) entry, we only check that the incomming
* interface is part of the static tree.
*/
cache_proxy = ipmr_cache_find_any_parent(mrt, vif);
if (cache_proxy &&
cache_proxy->mfc_un.res.ttls[true_vifi] < 255)
goto forward;
}
这段代码在处理一种特定的 (*, G) 场景。(*, G) 意味着“我不关心你是谁(源地址任意),只要你要去 G 组,我就按这个规则转”。
这里的逻辑稍微有点绕:它去找了一个“代理”缓存(cache_proxy)。如果这个代理缓存存在,并且在当前进来的接口(true_vifi)上的 TTL 阈值小于 255(意味着允许转发),那就直接跳到 forward 标签去干活。这是一个优化路径,允许 (*, G) 的共享树在特定条件下直接转发,而不必走更严格的匹配逻辑。
严苛的入接口检查(Wrong VIF)
如果上面的特殊路径没走通,接下来就是组播路由中最著名的“验明正身”环节。
/*
* Wrong interface: drop packet and (maybe) send PIM assert.
*/
if (mrt->vif_table[vif].dev != skb->dev) {
这行代码翻译成人话就是:路由条目说你应该从 eth0 进来,结果你却从 eth1 露头了。
这就是所谓的 WRONGVIF 错误。这种情况很常见,可能是因为网络拓扑变化了,也可能是有人配置错了。内核对此的处理非常严谨,分成了两种情况:
情况一:本机发出的包绕回来了(回环噩梦)
if (rt_is_output_route(skb_rtable(skb))) {
/* It is our own packet, looped back.
* Very complicated situation...
*
* The best workaround until routing daemons will be
* fixed is not to redistribute packet, if it was
* send through wrong interface. It means, that
* multicast applications WILL NOT work for
* (S,G), which have default multicast route pointing
* to wrong oif. In any case, it is not a good
* idea to use multicasting applications on router.
*/
goto dont_forward;
}
注意这里的注释语气——“Very complicated situation...”。这是一个连内核开发者都觉得棘手的边缘情况:路由器自己发出的组播包,因为某种路由配置错误,又从错误的接口绕回来了。
这就像你寄了一封信给自己,结果投递路线绕了一大圈最后从“收件箱”口又塞了进来。内核对此的态度是:我不掺和了(goto dont_forward)。直接丢弃,避免产生广播风暴或者逻辑死锁。注释里还吐槽说,直到路由守护进程被修好之前,这是最好的权宜之计——但同时也暗示,在路由器上跑组播应用程序本身就不是个好主意。
情况二:真的收错了(PIM Assert)
如果不是本机回环,那就是真的走错门了。这时候内核会统计错误,并考虑是否要吵架(发送 Assert 消息)。
cache->mfc_un.res.wrong_if++;
if (true_vifi >= 0 && mrt->mroute_do_assert &&
/* pimsm uses asserts, when switching from RPT to SPT,
* so that we cannot check that packet arrived on an oif.
* It is bad, but otherwise we would need to move pretty
* large chunk of pimd to kernel. Ough... --ANK
*/
(mrt->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(mrt, skb, true_vifi, IGMPMSG_WRONGVIF);
}
goto dont_forward;
}
这里有一系列的条件判断,仿佛是“吵架三连问”:
- 接口有效吗? (
true_vifi >= 0) - 允许断言吗? (
mrt->mroute_do_assert) - 是 PIM-SM 协议吗?或者 TTL 合理吗? 这里有个大长注释,解释了 PIM-SM 协议在从 RPT(共享树)切换到 SPT(最短路径树)时会用到 Assert,所以不能死板地检查包是否落在出接口上。为了不把半个
pimd(用户空间的 PIM 守护进程)搬到内核里去,这里做了一个折衷的判断。
如果条件都满足,且距离上次吵架(last_assert)已经超过了一定时间阈值(MFC_ASSERT_THRESH),内核就会调用 ipmr_cache_report()。
这就像路由器通过 IPC 对着用户空间的守护进程大喊一声:“喂!有个包从乱七八糟的接口进来了!你看着办!”(消息类型是 IGMPMSG_WRONGVIF``dont_forward。
转发循环与 TTL 阈值
如果入接口检查通过了,或者刚才的 (*, G) 特殊路径跳转过来了,我们就来到了 forward 标签。这里是转发逻辑的核心。
首先,更新入接口的统计信息(pkt_in, bytes_in),然后开始干活。
forward:
mrt->vif_table[vif].pkt_in++;
mrt->vif_table[vif].bytes_in += skb->len;
/*
* Forward the frame
*/
接下来的代码针对 (*, *)、(*, G) 和 (S, G) 有不同的处理逻辑。先看最特殊的 (*, *):
if (cache->mfc_origin == htonl(INADDR_ANY) &&
cache->mfc_mcastgrp == htonl(INADDR_ANY)) {
if (true_vifi >= 0 &&
true_vifi != cache->mfc_parent &&
ip_hdr(skb)->ttl >
cache->mfc_un.res.ttls[cache->mfc_parent]) {
/* It's an (*,*) entry and the packet is not coming from
* the upstream: forward the packet to the upstream
* only.
*/
psend = cache->mfc_parent;
goto last_forward;
}
goto dont_forward;
}
(*, *) 通常是一种通配规则,用于把包往上游转发。如果包不是从上游来的(true_vifi != cache->mfc_parent)且 TTL 也没衰减完,就把它往上游方向扔一个(psend 设为父接口),然后直接跳到发送逻辑。
核心转发循环:遍历 VIF 表
对于普通的 (S, G) 或 (*, G) 条目,内核会遍历 MFC 条目里记录的所有虚拟接口(vif_table),决定往哪些端口发。
for (ct = cache->mfc_un.res.maxvif - 1;
ct >= cache->mfc_un.res.minvif; ct--) {
/* For (*,G) entry, don't forward to the incoming interface */
if ((cache->mfc_origin != htonl(INADDR_ANY) ||
ct != true_vifi) &&
ip_hdr(skb)->ttl > cache->mfc_un.res.ttls[ct]) {
if (psend != -1) {
struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
if (skb2)
ipmr_queue_xmit(net, mrt, skb2, cache,
psend);
}
psend = ct;
}
}
这个循环是组播分发的精髓。它从 maxvif 倒着数到 minvif。对于每一个潜在的出口 ct,它会做两件决定生死的事:
- 是不是回头路? 如果是
(*, G),千万别把包再发回它进来的那个接口(ct != true_vifi),否则这就是个无限回环。 - TTL 够不够? 这就是著名的 TTL 阈值检查。每个接口都有一个阈值(
ttls[ct]),只有当数据包的 TTL(生存时间)大于这个值时,才允许从这个接口出去。这是控制组播范围、防止全网泛滥的最有效手段。
如果条件满足,且当前手里已经攒了一个待发的接口(psend != -1),说明需要发多个副本。于是内核 skb_clone() 一个新的 skb,调用 ipmr_queue_xmit() 把刚才那个接口的包发了。然后,把当前的这个接口 ct 记到 psend 里,准备在下一轮循环或者循环结束后发出去。
最后的收尾:本地交付与销毁
循环结束后,手里可能还捏着最后一个没发的接口索引(psend),或者可能一个都没有。到了 last_forward 标签:
last_forward:
if (psend != -1) {
if (local) {
struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
if (skb2)
ipmr_queue_xmit(net, mrt, skb2, cache, psend);
} else {
ipmr_queue_xmit(net, mrt, skb, cache, psend);
return 0;
}
}
dont_forward:
if (!local)
kfree_skb(skb);
return 0;
}
这里的逻辑处理了“本地投递”的标志。
- 如果
psend != -1(有地方要发):- 如果
local为真(说明本地主机也是接收者之一),我们必须clone一个包发给psend接口。因为原始的skb可能还需要交给上层协议栈(虽然在这个函数里似乎没看到直接交给本机的代码,通常在上层ip_mr_input里处理了,这里保留 skb 是为了保险)。 - 如果
local为假,说明纯转发,那就可以直接把原始 skb 给ipmr_queue_xmit(),省一次克隆,直接返回。
- 如果
最后,如果到了 dont_forward,说明包没被转发。如果本地也不收(!local),那就只能 kfree_skb(skb)——它的使命结束了。
至此,ip_mr_forward() 的任务完成。它像是一个冷酷而高效的分发中心,根据严格的规则(TTL、入接口匹配)把组播流复制到网络的各个角落。
那 ipmr_queue_xmit() 又做了什么呢?它负责把包真正塞到发送队列里去,还要处理可能存在的隧道封装。这是下一节的主题。