路由數據庫之路由查詢

這篇筆記以哈希方式的路由項組織方式,介紹了路由數據庫的查詢過程,相關代碼文件如下:

源代碼路徑 說明
net/ipv4/fib_hash.c 哈希方式的路由數據庫的實現文件
include/net/ip_fib.h 路由數據庫頭文件

1. 查詢條件: struct flowi

struct flowi {
	//輸入輸出網絡設備接口索引
	int	oif;
	int	iif;
	__u32	mark;
	//網絡層地址信息,這裏關注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)));

2. 查詢結果: struct fib_result

這些字段的值直接來自struct fib_info和struct fib_alias以及struct fib_node。

struct fib_result {
	unsigned char	prefixlen;
	unsigned char	nh_sel;
	unsigned char	type;
	unsigned char	scope;
	struct fib_info *fi;
#ifdef CONFIG_IP_MULTIPLE_TABLES
	struct fib_rule	*r;
#endif
};

3. 路由查詢: fn_hash_lookup()

真正的路由查詢過程涉及路由緩存、策略路由查詢和路由數據庫查詢三部分,對於路由數據庫查詢,又按照路由項的組織方式不同,可以有hash和tire兩種方式,這裏僅僅看hash方式的路由數據庫查詢過程。

如果最終需要查詢路由數據庫,那麼是通過fn_hash_lookup()完成的,該函數原型如下:

int fn_hash_lookup(struct fib_table *tb, const struct flowi *flp, struct fib_result *res);

對於輸入還是輸出都是該函數,所以首先需要了解下對於輸入和輸出路由查詢時,查詢條件各自都是如何構造的。

3.1 輸入路由查詢條件

輸入路由是由ip_rcv_finish()觸發,但是查詢條件ip_route_input_slow()中構造的,相關代碼如下:

static int ip_rcv_finish(struct sk_buff *skb)
{
	...
	if (skb->dst == NULL) {
		int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, skb->dev);
	}
	...
}
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 }; //輸入設備索引
...
}

3.2 輸出路由查詢條件

這裏以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) } },	//TCB中的tos字段來源需要進一步確定
			    .proto = sk->sk_protocol,
			    .uli_u = { .ports =
				       { .sport = inet->sport,
					 .dport = inet->dport } } };
				//scope未指定,那麼默認就是0,即UNIVERSE
	...
}

3.3 fn_hash_lookup()

注意:調用該函數時,已經明確了當前要查找的路由表,這是由策略路由決定的。

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;

	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) {
			//尋找key值相等的路由結點
			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表示路由查找成功,結束查找過程;
			//3. 小於0表示沒有路由,路由查詢失敗,結束查詢過程。對於支持策略路由的情況,
			//都不會這麼配置,而是將策略路由規則表的最後一條規則配置爲不可達,用來表示無路由
			if (err <= 0)
				goto out;
		}
	}
	//如果當前路由表沒有找到合適的路由,那麼返回1,對於策略路由,這會繼續下一條策略規則的匹配
	err = 1;
out:
	read_unlock(&fib_hash_lock);
	return err;
}

3.3.1 路由項匹配: 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;
	res->type = fa->fa_type;
	res->scope = fa->fa_scope;
	res->fi = fa->fa_info;
	//該路由項匹配了,增加其引用計數
	atomic_inc(&res->fi->fib_clntref);
	return 0;
}

4. 小結

從路由查詢過程可以看出,真正用於路由查詢的條件並不多:

  1. 目的地址,這是最關鍵的一個條件;
  2. TOS,基本上就是之IP首部的tos字段對應;而且如果路由項中如果不指定tos,那麼該條件忽略;
  3. scope:如果指定,那麼路由項的scope必須更加大,即離目的地址更近;
  4. 輸出接口:如果指定,那麼需要匹配;如果不指定,那麼該條件忽略;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章