FIB nexthop Exception是什么
理论
3.6版本内核移除了FIB查询前的路由缓存,取而代之的是下一跳缓存,这在路由缓存的前世今生 中已经说过了。本文要说的是在该版本中引入的另一个概念:FIB Nexthop Exception,用于记录下一跳的例外情形。
它有什么用呢?
内核通过查询转发信息表(fib_lookup),得到下一跳(fib_nh),从而得到了关于此条路由的相关信息,其中重要的包括下一跳设备nh_dev,下一跳网关nh_gw等。这些表项基本是稳定的。然而,但内核实际发包时,关于此条路由可能存在两个变数exception,其一是这条路由相关的路径MTU(PMTU)发生改变;其二是收到了关于此条路由的ICMP重定向报文。由于此两种改变并不是永久的,内核将他们保存在下一跳fib_nh的exception
- 收到过ICMP REDIRECT报文,表示之前发送的报文绕路了,之后的报文应该修改报文的下一跳。
- 收到过ICMP FRAGNEEDED报文,表示之前的报文太大了,路径上的一些设备不接受,需要源端进行分片。
这两种情况是针对单一目的地址的,什么意思呢?已PMTU为例,在下面的网络拓扑中,我在主机A上配置了下面一条路由

ip route add 4.4.0.0/16 via 4.4.4.4
意思是向所有目的地址在4.4.0.0/16的主机发包,下一跳都走192.168.99.1。
当A发送一个长度为1500的IP报文给C时 ,中间的一台网络设备B觉得这个报文太大了,因此它向A发送ICMP FRAGNEEDED报文,说我只能转发1300以下的报文,请将报文分片。A收到该报文后怎么办呢?总不能以后命中这条路由的报文全部按1300发送吧,因为并不是所有报文的路径都会包含B。
这时FIB Nexthop Exception就派上用场了,他可以记录下这个例外情况。当发送报文命中这条路由时,如果目的地址不是C,那么按1500进行分片,如果是C,则按1300进行分片。
实现
内核中使用fib_nh_exception表示这种例外表项
(include/net/ip_fib.h) struct fib_nh_exception { struct fib_nh_exception __rcu *fnhe_next; /* 冲突链上的下个fib_nh_exception结构 */ __be32 fnhe_daddr; /* 例外的目标地址 */ u32 fnhe_pmtu; /* 收到的ICMP FRAGNEEDED通告的PMTU */ __be32 fnhe_gw; /* 收到的ICMP REDIRECT通告的网关 */ unsigned long fnhe_expires; /* 该例外表项的过期时间 */ struct rtable __rcu *fnhe_rth; /* 关联的路由缓存 */ unsigned long fnhe_stamp; };
每一个下一跳结构fib_nh上有一个指针指向fnhe_hash_bucket哈希桶的指针:
struct fib_nh { ------------------------------- /* code omitted */ struct fnhe_hash_bucket *nh_exceptions; };
哈希桶在update_or_create_fnhe中创建,每个哈希桶包含2048条冲突链,每条冲突链可以存5个fib_nh_exception
以PMTU为例,在收到网络设备返回的ICMP FRAGNEEDED报文后,会调用下列函数将通告的pmtu值记录到fib_nh_exception上(也会记录到绑定的路由缓存rtable上)
static void __ip_rt_update_pmtu(struct rtable *rt, struct flowi4 *fl4, u32 mtu) { /* */ if (fib_lookup(dev_net(dst->dev), fl4, &res) == 0) { struct fib_nh *nh = &FIB_RES_NH(res); update_or_create_fnhe(nh, fl4->daddr, 0, mtu, jiffies + ip_rt_mtu_expires); } }
而在发包流程查询FIB之后,会首先看是否存在以目标地址为KEY的例外表项,如果有,就使用其绑定的路由缓存,如果没有就使用下一跳上的缓存
static struct rtable *__mkroute_output(const struct fib_result *res, const struct flowi4 *fl4, int orig_oif, struct net_device *dev_out, unsigned int flags) { /* code omitted */ if (fi) { struct rtable __rcu prth; struct fib_nh *nh = &FIB_RES_NH(*res); fnhe = find_exception(nh, fl4->daddr); // 查找 fl4->daddr 是否存在 fib_nh_exception if (fnhe) prth = &fnhe->fnhe_rth; // 如果有,直接使用其绑定的路由缓存 else { if (unlikely(fl4->flowi4_flags & FLOWI_FLAG_KNOWN_NH && !(nh->nh_gw && nh->nh_scope == RT_SCOPE_LINK))) { do_cache = false; goto add; } prth = __this_cpu_ptr(nh->nh_pcpu_rth_output); // 如果没有,使用下一跳上缓存的路由缓存 } rth = rcu_dereference(*prth); if (rt_cache_valid(rth)) { dst_hold(&rth->dst); return rth; } } }
举个例子:
主机中ip为192.168.1.201 gw为192.168.1.1 ,
添加路由ip route del 192.168.1.2 via 192.168.1.1 dev eth0;
也就以下数据包先到1.1网关然后再到1.2;

在生成路由项时,如果检测到报文的入口和出口设备相同,并且设备允许发送重定向报文,对于共享类型的链路OR源地址和网关在同一subnet 同一子网上,
设置重定向标志(IPSKB_DOREDIRECT)。
static int __mkroute_input(struct sk_buff *skb, const struct fib_result *res, struct in_device *in_dev, __be32 daddr, __be32 saddr, u32 tos) { struct fib_nh_common *nhc = FIB_RES_NHC(*res); struct net_device *dev = nhc->nhc_dev; struct fib_nh_exception *fnhe; struct rtable *rth; if (out_dev == in_dev && err && IN_DEV_TX_REDIRECTS(out_dev) && skb->protocol == htons(ETH_P_IP)) { __be32 gw; gw = nhc->nhc_gw_family == AF_INET ? nhc->nhc_gw.ipv4 : 0; if (IN_DEV_SHARED_MEDIA(out_dev) || inet_addr_onlink(out_dev, saddr, gw)) IPCB(skb)->flags |= IPSKB_DOREDIRECT; }
在forwarding函数中,发送ICMP重定向报文
int ip_forward(struct sk_buff *skb) { /* * We now generate an ICMP HOST REDIRECT giving the route * we calculated. */ if (IPCB(skb)->flags & IPSKB_DOREDIRECT && !opt->srr && !skb_sec_path(skb)) ip_rt_send_redirect(skb);
ICMP HOST REDIRECT向接收
对于code为ICMP_REDIR_HOST的ICMP重定向报文,如果其指定的新的网关(192.168.1.2)可达,邻居表存在。在路由存在的情况下,由函数update_or_create_fnhe更新或者创建一个exception项,超时时长设置为ip_rt_gc_timeout,默认为300秒,可通过PROC文件gc_timeout修改。
static void __ip_do_redirect(struct rtable *rt, struct sk_buff *skb, struct flowi4 *fl4, bool kill_route) { __be32 new_gw = icmp_hdr(skb)->un.gateway;// 新的网关 __be32 old_gw = ip_hdr(skb)->saddr; struct net_device *dev = skb->dev; -------------------------------- n = __ipv4_neigh_lookup(rt->dst.dev, new_gw);// 查找新的网关邻居表项 if (!n) n = neigh_create(&arp_tbl, &new_gw, rt->dst.dev); //创建neigh if (!IS_ERR(n)) { if (!(n->nud_state & NUD_VALID)) { neigh_event_send(n, NULL);// 检查neigh是否可用;发送邻居(ARP/NDP)事件。该函数的主要作用是更新邻居缓存并通知相关的网络协议栈某个邻居的状态发生了变化 } else { if (fib_lookup(net, fl4, &res, 0) == 0) { struct fib_nh_common *nhc; fib_select_path(net, &res, fl4, skb); nhc = FIB_RES_NHC(res); update_or_create_fnhe(nhc, fl4->daddr, new_gw, 0, false, jiffies + ip_rt_gc_timeout); } if (kill_route) rt->dst.obsolete = DST_OBSOLETE_KILL; call_netevent_notifiers(NETEVENT_NEIGH_UPDATE, n); } neigh_release(n); } return;
对于下一跳exception存在的情况,更新网关值以及fnhe_expires超时值。并且更新相应的路由缓存;
update_or_create_fnhe
fill_route_from_fnhe 更新路由缓存
if (fnhe) { if (fnhe->fnhe_genid != genid) fnhe->fnhe_genid = genid; if (gw) fnhe->fnhe_gw = gw; if (pmtu) { fnhe->fnhe_pmtu = pmtu; fnhe->fnhe_mtu_locked = lock; } fnhe->fnhe_expires = max(1UL, expires); /* Update all cached dsts too */ rt = rcu_dereference(fnhe->fnhe_rth_input); if (rt) fill_route_from_fnhe(rt, fnhe); rt = rcu_dereference(fnhe->fnhe_rth_output); if (rt) fill_route_from_fnhe(rt, fnhe); } else {
static void fill_route_from_fnhe(struct rtable *rt, struct fib_nh_exception *fnhe) { rt->rt_pmtu = fnhe->fnhe_pmtu; rt->rt_mtu_locked = fnhe->fnhe_mtu_locked; rt->dst.expires = fnhe->fnhe_expires; if (fnhe->fnhe_gw) { rt->rt_flags |= RTCF_REDIRECTED; rt->rt_uses_gateway = 1; rt->rt_gw_family = AF_INET; rt->rt_gw4 = fnhe->fnhe_gw; } }
重定向生成的路由项设置RTCF_REDIRECTED标志

浙公网安备 33010602011771号