主要参考了《深入Linux内核架构》和《精通Linux内核网络》相关章节
图30-1给出了在网络协议栈中路由子系统的位置(灰色框),这张图没有包含所有的细节(Netfilter、网桥等),但给出了横跨路由的其他主要的内核子系统。
本节重点介绍IPv4路由选择子系统及其使用的主要数据结构,如路由选择表、转发信息库(ForwardingInformation Base,FIB)和FIB别名、FIB TRIE等(顺便说一句,TRIE并非首字母缩写,而是由单词retrieval衍生而来的)。TRIE是一种特殊的树,它替代了FIB散列表。你将在本章学习路由选择子系统查找是如何进行的、在什么情况下生成ICMP重定向消息及如何生成它们、为何将路由选择缓存代码删除。
简单地讲,路由器就是一台网络设备,它配备多个网络接口卡(Network Interface Card,NIC)、能利用它的网络知识正确转发入口流量。
决定一个入口封包应当送给本地主机还是转发所需要的信息,以及在转发时正确转发封包所需要的信息,都存储在一个被称为转发信息库(Forwarding Information Base,FIB)的数据库中,它通常被简称为路由表(routing table)。
Linux网络栈最重要的目标之一是转发流量,对于Internet骨干中的核心路由器来说尤其如此。**Linux IP栈层被称为路由选择子系统,负责转发数据包和维护转发数据库。**对于小型网络,管理FIB的工作可由系统管理员手工完成,因为这种类型的网络拓扑几乎是静态的。而对于核心路由器来说,情况则有所不同,因为其拓扑是动态的,很多信息都在不断变化。在这种情况下,管理FIB的工作通常由用户空间路由选择守护程序负责,有时还结合使用特殊的硬件改进。这些用户空间守护程序通常用于维护独立的路由选择表,偶尔还会与内核路由选择表进行交互。
先来介绍基本知识:何为路由选择?来看一个非常简单的转发示例。你有两个以太网局域网LAN1和LAN2。其中,LAN1包含子网192.168.1.0/24,而LAN2包含子网192.168.2.0/24。在这两个LAN之间,有一台转发路由器,它有两个以太网网络接口卡,其中连接到LAN1的网络接口为etho,其IP地址为192.168.1.200,而连接到LAN2的网络接口为eth1,其IP地址为192.168.2.200,如图5-1所示。出于简化考虑,咱们假设转发路由器没有运行防火墙守护程序。你开始从LAN1向LAN2发送流量。对到来的数据包(它们从LAN1发送到LAN2或反向发送)进行转发的过程被称为路由选择,是根据被称为路由选择表的数据结构进行的。本章和下一章都将对这个过程以及路由选择表进行讨论。
在图5-1中,来自LAN1并前往LAN2的数据包到达etho后,通过外出设备eth1转发出去。在这个过程中,来到转发路由器的数据包从内核网络栈的第2层(数据链路层)移到第3层(网络层)。然而,不同于目的地为转发路由器的流量,无需将这些数据包移到第4层(传输层),因为这些流量无需由第4层的传输套接字进行处理,而应直接转发。移到第4层有一定的性能开销,最好尽可能避免。这些流量在第3层被处理。根据转发路由器配置的路由选择表,将这些数据包从出站接口eth1转发出去或将它们丢弃。
这里还需要说说另外两个常见的路由选择术语:默认网关和默认路由。在路由选择表中指定了默认网关条目时,不与其他路由选择条目匹配的数据包都将转发到默认网关,不管其IP报头中的目标地址是什么。使用无类域间路由选择(Classless Inter-Domain Routing,CIDR)表示法时,默认路由用0.0.0.0/0表示。
作为一个简单的示例,可按如下方法将IPv4地址为192.168.2.1的机器指定为默认网关。
ip route add default via 192.168.2.1
也可以使用如下route命令。
route add default gateway 192.168.2.1
本节介绍了转发的含义,并通过一个简单示例演示了如何在两个LAN之间转发数据包。还介绍了默认网关和默认路由是什么以及如何添加它们。至此,你了解了基本术语以及转发的含义,下面来看看如何在路由选择子系统中进行查找。
接收或发送每个数据包时,都必须在路由选择子系统中进行查找。在3.6版之前的内核中,无论在接收还是传输路径中,**查找都包含两个阶段:首先在路由选择缓存中查找;如果没有找到,再在路由选择表中查找(路由选择缓存将在5.4.3节讨论)。**查找工作由方法fib_lookup()完成。如果在路由选择子系统中找到了匹配的条目,方法fib_lookup()将创建一个包含各种路由选择参数的fib_result对象,并返回0。本节以及本章的其他部分都将讨论到fib_result对象。fib_lookup()的原型如下。
int fib_lookup(struct net *net,const struct flowi4 *flp,struct fib_result *res)
**flowi4对象包含对IPv4路由选择查找过程至关重要的字段,如目标地址、源地址、服务类型(TOS)等。事实上,flowi4对象定义了要在路由选择表中查找的键,必须在使用方法fib_lookup()执行查找前对其进行初始化。**对于IPv6,有一个类似的对象——flowi6。这两种对象都是在include/net/flow.h中定义的。
fib_result对象是在查找过程中生成的。
方法fib_lookup()首先在本地FIB表中搜索。如果没有找到,再在主FIB表中查找。
include\net\route.h
// 路由选择条目
struct rtable {
struct dst_entry dst; // 目录缓存,有input、output两个回调函数
int rt_genid;
unsigned int rt_flags;
__u16 rt_type;
__u8 rt_is_input; // 一个标志,对于输入路由设置为1
__u8 rt_uses_gateway; // 如果下一个为网关为1,下一个为直接路由为0
int rt_iif;
/* 有关邻居的信息 */
__be32 rt_gateway;
/* Miscellaneous cached information */
u32 rt_pmtu; // 路径mtu
u32 rt_table_id;
struct list_head rt_uncached;
struct uncached_list *rt_uncached_list;
};
请注意,内核3.6新增了方法fib_compute_spec_dst(),它将一个SKB作为参数。这使得结构rtable的成员rt_spec_dst显得多余,因此将其删除了。在特殊情况下,例如,在方法icmp_reply()中将收到的数据包的源地址作为目标地址向发送方发送应答时,需要使用方法fib_compute_ spec_dst()
**对于目的地为当前主机的单播数据包,将dst对象的input回调函数设置为ip_local_deliver();而对于需要转发的单播数据包,将input回调函数设置为ip_forward()。对于由当前主机生成并要向外发送的数据包,将output回调函数设置为ip_output()。**对于组播数据包,在有些情况下(本章不详细讨论)将input回调函数设置为ip_mr_input()。正如你将在本章后面的PROHIBIT规则示例中看到的,在有些情况下会将input回调函数设置为ip_error()。下面来看看fib_result对象。
对路由表查找后返回该结构。它的内容并不是简单地包含下一跳信息,而且包含有其他特性。例如,策略路由所需的更多参数。
struct fib_result {
unsigned char prefixlen; // 前缀长度
unsigned char nh_sel; // 下一跳数量
unsigned char type; // 处理数据包的方式
unsigned char scope;
u32 tclassid;
struct fib_info *fi; // 指向路由选择条目(存储了指向下一跳的引用)
struct fib_table *table; // FIB表
struct hlist_head *fa_head; // 使用fib_alias对象旨在优化路由选择条目
};
路由表是路由子系统的核心。简单地说,它是由一个路由数据库组成,IPv4的其他子系统通过各种函数可以使用该数据库,其中最重要的函数是路由查找。
FIB表
路由选择子系统的主数据结构是路由选择表,由结构fib_table表示。
简单地说,路由选择表的每个条目都指定了前往特定子网(或特定IPv4目标地址)的流量所对应的下一跳。当然,这些条目还包含本章将讨论的其他参数。**每个路由选择条目都包含一个fib_info对象( include/net/ip_fib.h ),其中存储了最重要的路由选择条目参数(但正如你在本章后面将要看到的,并非所有参数都存储在这里)。**fib_info对象由方法fib_create_info() ( netipv4/fib_semantics.c)创建,存储在散列表fib_info_hash中。路由使用prefsrc时,fib_info对象也将被加入到散列表fib_info_laddrhash中。
fib_info对象中有一个全局计数器——fib_info_cnt,方法fib_create_info()每次创建fib_info对象时都会将其加1,而方法free_fib_info()每次释放fib_info对象时又都将其减1。这个散列表增长到指定阈值时会自动调整大小。在散列表fib_info_hash中进行查找的工作是由方法fib_find_info()完成的。它在没有找到条目时返回NULL。以串行方式访问fib_info成员的工作由自旋锁( spinlock ) fib_info_lock完成。
struct fib_table {
struct hlist_node tb_hlist;
u32 tb_id;
int tb_num_default;
struct rcu_head rcu;
unsigned long *tb_data;
unsigned long __data[0];
};
不同路由表项之间可以共享一些参数,这些参数被存储在fib_info数据结构内。当一个新的路由表项所用的一组参数与一个已存在的路由项所用的参数匹配时,则重复使用已存在的fib_info结构。
FIB信息
路由选择条目由结构fib_info表示。它包含重要的路由选择条目参数,如出站网络设备( fib_dev)、优先级(fib_priority)、路由选择协议标识符(fib_protocol)等。下面来看看结构fib_info。
struct fib_info {
struct hlist_node fib_hash;
struct hlist_node fib_lhash;
struct net *fib_net; // 所属的网络命名空间
int fib_treeref; // 一个引用计数器,表示包含指向该fib_info对象的引用的fib_alias对象的数量。
atomic_t fib_clntref; // 引用fib_info对象自身的计数器
unsigned int fib_flags;
unsigned char fib_dead;
unsigned char fib_protocol;
unsigned char fib_scope;
unsigned char fib_type;
__be32 fib_prefsrc;
u32 fib_tb_id;
u32 fib_priority;
struct dst_metrics *fib_metrics;
#define fib_mtu fib_metrics->metrics[RTAX_MTU-1]
#define fib_window fib_metrics->metrics[RTAX_WINDOW-1]
#define fib_rtt fib_metrics->metrics[RTAX_RTT-1]
#define fib_advmss fib_metrics->metrics[RTAX_ADVMSS-1]
int fib_nhs;
#ifdef CONFIG_IP_ROUTE_MULTIPATH
int fib_weight;
#endif
unsigned int fib_offload_cnt;
struct rcu_head rcu;
struct fib_nh fib_nh[0];
#define fib_dev fib_nh[0].nh_dev
};
fib_treeref:一个引用计数器,表示包含指向该fib_info对象的引用的fib_alias对象的数量。在方法fib_create_info()和fib_release_info()中,将分别对这个引用计数器进行加1和减1操作。上述两个方法都位于net/ipv4/fib_semantics.c中。
fib_clntref:一个引用计数器,方法fib_create_info() ( net/ipv4/fib_semantics.c)会将其加1,而fib_info_put() ( include/ net/ip_fib.h)则将其减1。方法fib_info_put()将它减1后,如果它变成了0,fib_info对象就会被方法free_fib_info()释放。
fib_dead:一个标志,指出了是否允许方法free_fib_info()将fib_info对象释放。在调用方法free_fib_info()前,必须将fib_dead设置为1。如果没有设置fib_dead标志(其值为0 ),fib_info对象将被视为处于活动状态,则若试图调用方法free_fib_info()来释放它,将以失败告终。
fib_protocol:路由的路由选择协议标识符。从用户空间添加路由选择规则时,如果没有指定路由选择协议ID,fib_protocol将被设置为RTPROT_BOOT。管理员添加路由时,可能会使用修饰符proto static,指出路由是由管理员添加的。例如,可以像下面这样做:ip route add proto static 192.168.5.3 via 192.168.2.1。fib_protocol可设置为下面的标志之一。
路由选择表也可能是由用户空间路由选择守护程序(如ZEBRA、XORP、MROUTED等)添加的。在这种情况下,将指定相应的协议标识符(请参阅include/uapilinux/rtnetlink.h中RTPROT_XXX的定义)。例如,对于守护程序XORP,将指定RTPROT_XORP。请注意,这些标志(如RTPROT_KERNEL和RTPROT_STATIC)也被IPv6用来设置相应的字段(结构rt6_info的字段rt6i_protocol,对应于rtable的IPv6对象是rt6_info )。
fib_scope:目标地址的范围( scope ),它为地址和路由都指定了范围。简单地说,范围指出了主机相对于其他结点的距离。命令ip address show会显示主机配置的所有IP地址的范围,而命令ip route show会显示主表中所有路由条目的范围。范围分下面几种。
管理员添加路由时,如果没有指定范围,将根据下述规则设置fib_scope字段的值。
fib_type:路由的类型。fib_type字段是内核3.7在结构fib_info中新增的。它们被用作键,旨在确保能够根据类型区分不同的fib_info对象。以前,只是在FIB别名(fib_alias )的fa_type字段中存储了这种类型。你可以添加规则,禁止特定的流量通过。例如,可以像下面这样做: ip route add prohibit 192.168.1.17 from 192.168.2.103。
fib_prefsrc:有时候,你可能想将查找键指定为特定的源地址,为此可设置fib_prefsrc。fib_priority:路由的优先级,默认为0,表示最高优先级。值越大,表示优先级越低。例如,优先级3比最高优先级0低。可使用以下面方式的ip命令之一配置优先级。
这3个命令都将fib_priority设置为了5,它们没有任何差别。另外,命令ip route的参数metric与结松fib info的字段fib metrics没有任何关系
fib_mtu、 fib_window 、 fib_rtt和fib_advmss只是数组fib_metrics中常用元素的别名。fib_metrics是一个包含15 (RTAX_MAX)个元素的数组,存储了各种指标,被初始化为net/core/dst.c中定义的dst_default_metrics。很多指标都与TCP协议相关,如初始拥塞窗口( initcwnd )。本章末尾的表5-1列出了所有指标,并指出了它们是否与TCP相关。在用户空间中,可以这样设置TCPv4指标initcwnd。
有些指标并非TCP专用的,如mtu。在用户空间中,可以这样设置指标mtu;
也可以这样设置它:
这两个命令的差别在于:指定了修饰符lock时,不会尝试路径MTU发现;而没有指定修饰符lock时,内核可能基于路径MTU发现更新MTU。要更详细地了解这是如何实现的,请参阅net/ipv4/route.c中的方法__ip_rt_update_pmtu()。
fib_nhs:下一跳的数量。没有设置多路径路由选择(CONFIG_IP_ROUTE_MULTIPATH)时,其值不能超过1。多路径路由选择功能为路由指定了多条替代路径,并可能给这些路径指定不同的权重。这种功能提供了诸如容错、增加带宽和提高安全性等好处,这将在第6章讨论。
fib_dev:将数据包传输到下一跳的网络设备。
fib_nh[o]:表示下-跳。使用多路径路由选择时,可在一条路由中指定多个下一跳。在这种情况下,将有一个下一跳数组。
例如,要指定两个下一跳结点,可以这样做:
**前面说过,fib_type为RTN_PROHIBIT时,将发送一条ICMPv4“数据包被过滤掉”消息( ICMP_PKT_FILTERED)。**这是如何实现的呢?在netipv4/fib_semantics.c中,定义了一个数组,它包含12(RTN_MAX)个fib_props对象。这个数组使用的索引为路由类型。可能的路由类型(如RTN_PROHIBIT和RTN_UNICAST )可在include/uapi/linux/rtnetlink.h中找到。这个数组的每个元素都是结构fib_prop的实例。结构fib_prop非常简单,其代码实现如下。
struct fib_prop {
int error;
u8 scope;
};
对于每种路由类型,相应的fib_prop对象都包含其error和scope。例如,对于极其常见的路由类型RTN_UNICAST(经由网关的路由或直连路由),error为0(表示没有错误),而scope为RT_SCOPE_UNIVERSE;对于路由类型RTN_PROHIBIT(系统管理员为禁止流量通过而配置的规则),error 为-EACCES,而scope为RT_SCOPE_UNIVERSE。
对于每种路由类型,相应的fib_prop对象都包含其error和scope。例如,对于极其常见的路由类型RTN_UNICAST(经由网关的路由或直连路由),error为0(表示没有错误),而scope为RT_SCOPE_UNIVERSE;对于路由类型RTN_PROHIBIT(系统管理员为禁止流量通过而配置的规则),error 为-EACCES,而scope为RT_SCOPE_UNIVERSE。
就这里介绍的情形而言,方法fib_lookup()(由它发起在IPv4路由选择子系统中进行查找的操作)最终将返回错误-EACCES。这种错误从check_leaf()出发,不断向后传播,经fib_table_lookup()等最终到达触发调用链的方法,即fib_lookup()。在接收路径中,方法fib_lookup()返回错误时,将由方法ip_error()进行处理,根据错误采取相应的措施。就错误-EACCES而言,采取的措施是发回一条代码为“数据包被过滤掉”(ICMP_PKT_FILTERED)的ICMPv4“目的地不可达”消息,同时将数据包丢弃。
缓存路由选择查找结果是一种优化技术,可用于改善路由选择子系统的性能。**路由选择查找结果通常缓存在下一跳对象(fib_nh)中,但在数据包不是单播数据包或使用了realms(数据包的itag不为0)时,则不会将查找结果缓存到下一跳中。这是因为,如果缓存所有数据包类型的查找结果,将导致不同类型的路由使用相同的下一跳,而这种情况必须避免。**还存在其他一些不那么重要的例外情况,但本章不作讨论。在接收路径和传输路径中进行缓存的过程如下。
nh_pcpu_rth_output是一个基于CPU的变量,用以改善性能。这意味着对于每个CPU,都有一个输出条目dst的副本。几乎在任何情况下都需要使用缓存,只有为数不多的几种例外情形,如发送ICMPv4重定向消息、设置了itag ( tclassid)以及没有足够的内存等。
本小节介绍了使用下一跳对象来实现缓存的过程,下一小节将讨论表示下一跳的结构fib_nh以及FIB下一跳例外(FIB nexthop exception )。
结构fib_nh表示下一跳,包含诸如外出网络设备(nh_dev)、外出接口索引(nh_oif)、范围(nh_scope)等信息,如下所示。
struct fib_nh {
struct net_device *nh_dev; // 外出网络设备
struct hlist_node nh_hash;
struct fib_info *nh_parent;
unsigned int nh_flags;
unsigned char nh_scope; // 范围
#ifdef CONFIG_IP_ROUTE_MULTIPATH
int nh_weight;
atomic_t nh_upper_bound;
#endif
#ifdef CONFIG_IP_ROUTE_CLASSID
__u32 nh_tclassid;
#endif
int nh_oif; // 外出接口索引
__be32 nh_gw;
__be32 nh_saddr;
int nh_saddr_genid;
struct rtable __rcu * __percpu *nh_pcpu_rth_output;
struct rtable __rcu *nh_rth_input;
struct fnhe_hash_bucket __rcu *nh_exceptions;
struct lwtunnel_state *nh_lwtstate;
};
**nh_dev字段指出了将流量传输到下一跳所使用的网络设备( net_device对象)。**与一条或多条路由相关联的网络设备被禁用时,将发送NETDEV_DOWN通知。处理这种事件的FIB回调函数为方法fib_netdev_event()。它是通知者对象fib_netdev_notifier的回调函数,是在方法ip_fib_init()中调用方法register_netdevice_notifier()注册的(通知链将在第14章讨论)。收到通知NETDEV_DOWN后,方法fib_netdev_event()调用方法fib_disable_ip()。在方法fib_disable_ip(中,执行的步骤如下。
FIB下一跳例外
FIB下一跳例外( nexthop exceptions )是3.6版内核新增的,旨在处理这样的情形,即路由选择条目变更并非用户空间操作引起的,而是ICMPv4重定向消息或路径MTU发现导致的。使用的散列键为目标地址。FIB下一跳例外基于包含2048个条目的散列表。释放散列表条目时,始于长度为5的链条。每个下一跳对象(fib_nh)都包含一个FIB下一跳例外散列表——nh_exceptions,它是一个fnhe_hash_bucket结构实例。
策略路由背后的主要思想是允许用户除了可以根据目的IP地址配置路由外,还可以根据其他多个参数来配置路由。
在因特网繁荣稳定地发展的这些年中,大多数路由器被配置为只能够根据封包内的目的IP地址来路由封包。(为了简化,这里不考虑跨越多个ISP或多个国家等因素)。只基于目的地址(以及一些额外的配置参数)的路由,使路由表在面临大多数情况时都是最优的。
但商业界还需要考虑更多的因素,例如,出于安全或计费考虑需要分开流量,或者通过单独的路由发送实时数据流。这就出现了策略路由,由于路由可以有各种各样的标准,在本章中,我们将除基于目的地址外的路由都认为是策略路由。
不使用策略路由选择(没有设置CONFIG_IP_MULTIPLE_TABLES时),将创建两个路由选择表:本地表和主表。主表的ID为254 (RT_TABLE_MAIN ),本地表的ID为255( RT_TABLE_LOCAL)。本地表包含针对本地地址的路由选择条目,只有内核才能在本地表中添加路由选择条目。
**在主表(RT_TABLE_MAIN )中添加路由选择条目的工作是由系统管理员完成的(例如,使用命令ip route add )。**这些表由net/ipv4/fib_frontend.c中的方法fib4_rules_init()创建,它们在2.6.25版之前的内核中分别被命名为ip_fib_local_table和ip_fib_main_table,但为了支持以统一的方式(通过方法fib_get_table()和合适的参数)访问路由选择表,现在已不再使用这些名称。
所谓以统一的方式访问,指的是无论是否启用了策略路由选择,通过方法fib_get_table()访问路由选择表的方式都相同。方法fib_get_table()只接受两个参数:网络命名空间和表ID。请注意,还有一个用于策略路由选择的方法——fib4_rules_init(),它位于net/ipv4/fib_rules.c中。在支持策略路由选择的情况下将调用它。当支持策略路由选择(设置了CONFIG_IP_MULTIPLE_TABLES)时,默认有3个表(本地表、主表和默认表),且最多可以有255个路由选择表。
策略路由选择将在第6章讨论。要访问主表,可以采取如下做法。
使用系统管理员命令ip route或route。
有时候,路由选择条目可能是次优的。在这种情况下,将发送ICMPv4重定向消息。次优条目的主要判断标准是:输入设备和输出设备相同。但正如你即将在本节看到的,还必须满足其他条件,才会发送ICMPv4重定向消息。
ICMPv4重定向消息的代码有4种:
下图演示了一种存在次优路由的情形。图中有3台机器,它们位于同一个子网( 192.168.2.0/24)中,并通过一个网关(192.168.2.1)相连。
生成ICMPv4重定向消息
存在次优路由时,将发送ICMPv4重定向消息。判断次优路由的最重要标准是:输人设备和输出设备相同,但同时还需满足其他一些条件。ICMPv4重定向消息的生成过程分如下两个阶段。
在方法_mkroute_input()中,必要时设置标志RTCF_DOREDIRECT。
在方法ip_forward()中,实际上是通过调用方法ip_rt_send_redirect()来发送ICMPv4重定向消息。
接收ICMPv4重定向消息
仅当ICMPv4重定向消息通过一些完整性检查后,才会对其进行处理,处理ICMPv4重定向消息的工作是由__ip_do_redirect()完成。