IPv4路由数据库之hash方式路由表查询

博客介绍了IPv4路由查询中的关键数据结构flowi和fib_result,详细阐述了路由查询条件的构造,包括输入和输出路由查询条件的设定,并解析了fn_hash_lookup()函数在路由表查询中的作用以及fib_semantic_match()如何进行路由项匹配。
摘要由CSDN通过智能技术生成


这篇笔记介绍了哈希方式路由表的查询过程。hash方式路由表的路由查询回调为fn_hash_lookup()。涉及的文件有:

源代码路径说明
include/net/ip_fib.hIPv4路由数据库头文件
core/ipv4/fib_hash.c哈希方式的路由数据库相关实现
core/ipv4/fib_semantics.c路由项信息管理相关函数实现

数据结构

路由查询条件: flowi

flowi是flow identify的简写,因为对于一条流来说,它们的路由查询条件往往是一致的。

struct flowi {
	// 输入输出网络设备对象索引
	int	oif;
	int	iif;
	__u32	mark;
	// L3层相关查询条件,这里只关注IPv4
	union {
		struct {
			__be32 daddr;
			__be32 saddr;
			__u8 tos;
			__u8 scope;
		} ip4_u;	
		struct {
			struct in6_addr	daddr;
			struct in6_addr	saddr;
			__be32 flowlabel;
		} ip6_u;
		struct {
			__le16 daddr;
			__le16 saddr;
			__u8 scope;
		} dn_u;
	} nl_u;
#define fld_dst		nl_u.dn_u.daddr
#define fld_src		nl_u.dn_u.saddr
#define fld_scope	nl_u.dn_u.scope
#define fl6_dst		nl_u.ip6_u.daddr
#define fl6_src		nl_u.ip6_u.saddr
#define fl6_flowlabel	nl_u.ip6_u.flowlabel
#define fl4_dst		nl_u.ip4_u.daddr
#define fl4_src		nl_u.ip4_u.saddr
#define fl4_tos		nl_u.ip4_u.tos
#define fl4_scope	nl_u.ip4_u.scope
	// L4层协议类型
	__u8 proto;
	__u8 flags;
	// L4层相关查询条件
	union {
		struct {
			__be16 sport;
			__be16 dport;
		} ports;
		struct {
			__u8 type;
			__u8 code;
		} icmpt;
		struct {
			__le16 sport;
			__le16 dport;
		} dnports;
		__be32 spi;
		struct {
			__u8 type;
		} mht;
	} uli_u;
#define fl_ip_sport	uli_u.ports.sport
#define fl_ip_dport	uli_u.ports.dport
#define fl_icmp_type	uli_u.icmpt.type
#define fl_icmp_code	uli_u.icmpt.code
#define fl_ipsec_spi	uli_u.spi
#define fl_mh_type	uli_u.mht.type
	__u32 secid; /* used by xfrm; see secid.txt */
} __attribute__((__aligned__(BITS_PER_LONG/8)));

可见,查询条件中大多数信息实际上就是来自数据包的L3层和L4层协议报头。

路由查询结果: fib_result

这些字段的值直接来自路由项的fib_info、fib_alias以及fib_node对象。

struct fib_result {
	unsigned char prefixlen; // 子网掩码长度
	unsigned char nh_sel; // 下一跳的索引为fi->fib_nh[nh_sel],多路径情况下才会大于0
	unsigned char type; // 路由项类型,非常重要,会影响查询完毕后的处理逻辑
	unsigned char scope; // 路由项作用域
	struct fib_info *fi; // 指向fib_info对象
#ifdef CONFIG_IP_MULTIPLE_TABLES
	struct fib_rule	*r; // 指向命中的策略路由规则
#endif
};

路由查询

为了更好的理解路由表的查询过程,这里先看下内核对于输入和输出方向的数据包是如何指定路由查询条件的。

输入路由查询条件

输入路由是由ip_rcv_finish()触发,但是查询条件是在ip_route_input_slow()中构造的,相关代码如下:

static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			       u8 tos, struct net_device *dev)
{
	struct flowi fl = {
	    .nl_u = {
	        .ip4_u = {
	            .daddr = daddr,
				.saddr = saddr,
				.tos = tos, // tos字段就是来自IP首部的TOS
				.scope = RT_SCOPE_UNIVERSE, // 作用域为UNIVERSE,最大
			}
	    },
		.mark = skb->mark, // mark来自数据包中的mark标记,一般该标记在过magle表时设置
		.iif = dev->ifindex // 输入设备索引
	};
...
}

输出路由查询条件

这里以ip_queue_xmit()中的条件为例看输出路由查询条件是如何指定的,实际使用中,可能更高层的协议(比如TCP)在调用IP层接口发送数据前就已经进行了路由查询,不过查询条件的指定方式是一致的。

#define RT_CONN_FLAGS(sk)   (RT_TOS(inet_sk(sk)->tos) | sock_flag(sk, SOCK_LOCALROUTE))

int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
{
	...
	struct flowi fl = { 
	    .oif = sk->sk_bound_dev_if, // 可以通过SO_BINDTODEVICE选项将某个socket绑定到某个网卡
		.nl_u = {
		    .ip4_u = {
		        .daddr = daddr,
				.saddr = inet->saddr,
				.tos = RT_CONN_FLAGS(sk) // 类似的可以通过socket选项设置TCB中的tos字段
				// scope未指定,那么默认就是0,即UNIVERSE
		    }
		},
		.proto = sk->sk_protocol,
		.uli_u = {
		    .ports = {
		        .sport = inet->sport,
				.dport = inet->dport
		    } 
		}
	};
	...
}

路由表查询: fn_hash_lookup()

注意:调用该函数时,已经明确了要查找的路由表,这是由策略路由决定的。

@tb: 要查询的路由表;
@flp: 路由查询条件;
@res:保存查询结果;
static int fn_hash_lookup(struct fib_table *tb, const struct flowi *flp, struct fib_result *res)
{
	int err;
	struct fn_zone *fz;
	struct fn_hash *t = (struct fn_hash*)tb->tb_data; // hash方式路由表私有数据结构

	read_lock(&fib_hash_lock);
	// 前面有介绍到,fn_zone_list是按照子网掩码由长到短顺序排列的,所以这里实现的
	// 的效果就是会从具体到一般的顺序进行路由项匹配
	for (fz = t->fn_zone_list; fz; fz = fz->fz_next) {
		struct hlist_head *head;
		struct hlist_node *node;
		struct fib_node *f;
		// 每个路由区代表一个子网掩码,这里将查询条件中的目的地址和子网掩码相与与后得到目的网络地址,
		// 即key,进而找到路由结点所在冲突链,然后遍历冲突链找到该网络对应的路由结点
		__be32 k = fz_key(flp->fl4_dst, fz);
		head = &fz->fz_hash[fn_hash(k, fz)];
		hlist_for_each_entry(f, node, head, fn_hash) {
			if (f->fn_key != k)
				continue;
			// 找到了路由结点,说明存在到达该网络的路由,进一步检查路由项的其它条件是否满足要求
			err = fib_semantic_match(&f->fn_alias, flp, res, f->fn_key, fz->fz_mask, fz->fz_order);
			// 1. 大于0会继续查询后面的路由;
			// 2. 等于0代表路由查找成功,结果已经保存在了res中,结束查找过程;
			// 3. 小于0表示没有路由,路由查询失败,此时策略路由也会结束后续查询过程;
			if (err <= 0)
				goto out;
		}
	}
	// 当前路由表没有找到合适的路由,那么返回1,此时策略路由会继续下一条策略规则的匹配
	err = 1;
out:
	read_unlock(&fib_hash_lock);
	return err;
}

路由项匹配: fib_semantic_match()

如上,调用该函数时已经找到了到达条件指定的目的网络的路由结点。该函数用于从该路由结点中找到一个符合条件的路由项。

@head:路由结点的fa_alias列表
@zone: 网络地址的大端表示;
@mask:子网掩码的大端表示;
@prefixlen:子网掩码长度
int fib_semantic_match(struct list_head *head, const struct flowi *flp,
		       struct fib_result *res, __be32 zone, __be32 mask, int prefixlen)
{
	struct fib_alias *fa;
	int nh_sel = 0;

	list_for_each_entry_rcu(fa, head, fa_list) {
		int err;
		// 路由项中配置了tos,才会检查tos是否匹配,如果没有配置,那么该字段就不作为匹配条件
		if (fa->fa_tos && fa->fa_tos != flp->fl4_tos)
			continue;
		// 路由项的作用域值越大,表示本机距离路由项表示的目的地址越近。这里要求路由项的作用域要比
		// 查询条件的作用域大,符合设计思想,即通过该路由项路由后,数据包要离目的地址更近
		if (fa->fa_scope < flp->fl4_scope)
			continue;
		// 标记该fa_alias被访问过了,它影响路由缓存的刷新,要注意设置它和选中该路由并没有关系
		fa->fa_state |= FA_S_ACCESSED;
		// fib_props这个全局变量的设计没有理解
		err = fib_props[fa->fa_type].error;
		if (err == 0) {
			struct fib_info *fi = fa->fa_info;
			// 该路由项被设置了DEAD标记,表示该路由项即将被删除,这种路由不能使用了,继续查询下一条
			if (fi->fib_flags & RTNH_F_DEAD)
				continue;
			// 寻找下一跳地址fib_nh
			switch (fa->fa_type) {
			case RTN_UNICAST:
			case RTN_LOCAL:
			case RTN_BROADCAST:
			case RTN_ANYCAST:
			case RTN_MULTICAST:
				for_nexthops(fi) {
					if (nh->nh_flags&RTNH_F_DEAD) // 下一跳地址可用
						continue;
					// 如果查询条件指定了出口地址,则路由项的出口地址要匹配
					if (!flp->oif || flp->oif == nh->nh_oif)
						break;
				}
#ifdef CONFIG_IP_ROUTE_MULTIPATH
				if (nhsel < fi->fib_nhs) {
					nh_sel = nhsel;
					goto out_fill_res;
				}
#else
				if (nhsel < 1) { // 填充路由查询结果
					goto out_fill_res;
				}
#endif
				endfor_nexthops(fi);
				continue;
			default:
				printk(KERN_WARNING "fib_semantic_match bad type %#x\n", fa->fa_type);
				return -EINVAL;
			}
		}
		return err;
	}
	return 1;
out_fill_res:
	// 这里填充查询结果,注意理解各个字段的含义
	res->prefixlen = prefixlen;
	res->nh_sel = nh_sel; // 表示fib_info对象中的下一跳的索引(可能有多个)
	res->type = fa->fa_type;
	res->scope = fa->fa_scope;
	res->fi = fa->fa_info;
	// 该路由项匹配了,增加其引用计数
	atomic_inc(&res->fi->fib_clntref);
	return 0;
}

从路由查询过程可以看出,真正用于路由查询的条件并不多:

  1. 目的地址,这是最关键的一个条件;
  2. TOS,就是IP首部的tos字段;而且如果路由项中如果不指定tos,那么该条件忽略;
  3. scope:如果指定,那么路由项的scope必须更加大,即通过路由后,数据包要离目的地址更近;
  4. 输出接口:如果指定,那么需要匹配;如果不指定该条件也会被忽略;
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值