在試驗環境中,網絡設備接口mylo的IP地址是127.10.0.1,它在內核中的表示是struct net_device myloopback_dev,測試程序往IP地址127.10.0.1發送DUMMY協議的數據報,協議棧爲其生成的路由目的入口如下:
the dst_entry:
the dev name: mylo
the error: 0
the obsolete: 0
the flag: DST_HOST
expires: 0, now: 110343
header len: 0
rt_flag: RTCF_LOCAL
rt_type: RTN_LOCAL
rt_dst: 127.10.0.1
rt_src: 127.10.0.1
rt_iif: 4
rt_gateway: 127.10.0.1
rt_spec_dst: 127.10.0.1
試驗環境中的網絡設備接口eth0的IP地址是172.16.48.2,測試程序往該IP地址所在子網內的IP地址172.16.48.1發送DUMMY協議的數據報,協議棧爲其生成的路由目的入口如下:
the dst_entry:
the dev name: eth0
the error: 0
the obsolete: 0
the flag: DST_HOST
expires: 0, now: 850858
header len: 0
rt_flag: 0
rt_type: RTN_UNICAST
rt_dst: 172.16.48.1
rt_src: 172.16.48.2
rt_iif: 2
rt_gateway: 172.16.48.1
rt_spec_dst: 172.16.48.2IP層路由適配(IP route)
路由表以及規則組成的系統,可以完成路由的管理以及查找的工作,但是爲了使得IP層的路由工作更加的高效,linux的路由體系裏,route.c裏完成大多數IP層與RPDB的適配工作,以及路由緩衝(route cache)的功能。
調用接口
IP層的路由接口分爲發送路由接口以及接收路由接口:
發送路由接口
IP層在發送數據時如果需要進行路由工作的時候,就會調用ip_route_out函數。這個函數在完成一些鍵值的簡單轉換以後,就會調用ip_route_output_key函數,這個函數首先在緩存裏尋找路由,如果失敗就會調用ip_route_output_slow,ip_route_output_slow裏調用fib_lookup在路由表裏尋找路由,如果命中,首先在緩存裏添加這個路由,然後返回結果。
ip_route_out route.h ip_route_output_key route.c 1984; ip_route_output_slow route.c 1690;" |
接收路由接口 IP層接到一個數據包以後,如果需要進行路由,就調用函數ip_route_input,ip_route_input現在緩存裏尋找,如果失敗則ip_route_inpu調用ip_route_input_slow, ip_route_input_slow裏調用fib_lookup在路由表裏尋找路由,如果命中,首先在緩存裏添加這個路由,然後返回結果。
ip_route_input_slow route.c 1312;" f ip_route_input route.c 1622;" f |
cache 路由緩存保存的是最近使用的路由。當IP在路由表進行路由以後,如果命中就會在路由緩存裏增加該路由。同時系統還會定時檢查路由緩存裏的項目是否失效,如果失效則清除。
**************
ip_route_input()->
1—— skb->dst = (struct dst_entry*)rth;//在緩存中找到路由信息。
2—— ip_route_input_slow()->
____判斷出是要轉發 rth->u.dst.input = ip_forward;
rth->u.dst.output = ip_output;
____判斷出是要自己接受 rth->u.dst.output=ip_output;
;rth->u.dst.input = ip_local_deliver;
err = rt_intern_hash(hash, rth, (struct rtable**)&skb->dst);//將查到的路由信息掛在skb->dst上,同時放入路由緩存。
這回該明白 return skb->dst->input(skb);是幹什麼去了吧?
- //萬惡的我爲了偷樑換柱,redifine了nf_conn_nat
- struct nf_conn_nat {
- struct rtable *rth;
- };
-
- static struct nf_ct_ext_type route_extend __read_mostly = {
- .len = sizeof(struct nf_conn_nat),
- .align = __alignof__(struct nf_conn_nat),
- .id = NF_CT_EXT_NAT,
- .flags = NF_CT_EXT_F_PREALLOC,
- };
- //設置conntrack的rtable
- static void conn_dst_set(struct nf_conn *ct, struct rtable *dst)
- {
- struct nf_conn_nat *rt = nf_ct_ext_find(ct, NF_CT_EXT_NAT);
- if (rt == NULL) {
- rt = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC);
- if (rt == NULL) {
- return;
- }
- rt->rth = NULL;
- }
- #include <net/dst.h>
- if (rt->rth == NULL ||
- ((rt->rth != NULL) && rt->rth->u.dst.output == dst_discard)) {
- dst_use(&dst->u.dst, jiffies);
- rt->rth = dst;
- }
- }
-
- static void save_dst(struct sk_buff *skb, struct nf_conn *ct)
- {
- struct rtable *rth;
- rcu_read_lock_bh();
- rth = skb_rtable(skb);
- if (rth != NULL) {
- conn_dst_set(ct, rth);
- }
- rcu_read_unlock_bh();
- }
-
- static unsigned int ipv4_confirm(unsigned int hooknum,
- struct sk_buff *skb,
- const struct net_device *in,
- const struct net_device *out,
- int (*okfn)(struct sk_buff *))
- {
- struct nf_conn *ct;
- enum ip_conntrack_info ctinfo;
- const struct nf_conn_help *help;
- const struct nf_conntrack_helper *helper;
- unsigned int ret;
-
- /* This is where we call the helper: as the packet goes out. */
- ct = nf_ct_get(skb, &ctinfo);
- //僅僅針對FORWARD包進行路由cache,因此判斷HOOKNUM和sock
- if (ct && hooknum == NF_INET_POST_ROUTING && skb->sk == NULL &&
- ct != &nf_conntrack_untracked) {
- save_dst(skb, ct);
- }
- if (!ct || ctinfo == IP_CT_RELATED + IP_CT_IS_REPLY)
- goto out;
- ....
- }
-
- static unsigned int ipv4_conntrack_in(unsigned int hooknum,
- struct sk_buff *skb,
- const struct net_device *in,
- const struct net_device *out,
- int (*okfn)(struct sk_buff *))
- {
- unsigned int ret = nf_conntrack_in(dev_net(in), PF_INET, hooknum, skb);
- //僅僅在PRE_ROUTING檢查過路包
- if (ret == NF_ACCEPT && hooknum == NF_INET_PRE_ROUTING) {
- enum ip_conntrack_info ctinfo;
- struct nf_conn *ct;
- struct rtable *rth;
- struct nf_conn_nat *rt;
- ct = nf_ct_get(skb, &ctinfo);
- if (!ct) {
- goto out;
- }
- rcu_read_lock_bh();
- rt = nf_ct_ext_find(ct, NF_CT_EXT_NAT);
- if (rt == NULL) {
- rt = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC);
- if (rt == NULL) {
- rcu_read_unlock_bh();
- goto out;
- }
- rt->rth = NULL;
- }
- if ((rth = rt->rth) == NULL) {
- rcu_read_unlock_bh();
- goto out;
- }
- dst_use(&rth->u.dst, jiffies);
- //以下將conn的路由cache設置進skb,如此一來就不用ROUTING了
- skb_dst_set(skb, dst_clone(&rth->u.dst));
- rcu_read_unlock_bh();
- //注意以下的被註釋的代碼,實際上放開這些註釋的話,所實現的功能和不放開註釋
- //的效果是完全不同的!以下的註釋可以實現HOOK點間的跳轉,十分方便和硬卡進行
- //接口,你可以在NF_HOOK那一行調用硬卡接口實現直接發送,然後返回NF_STOLEN
- // NF_HOOK(PF_INET, NF_INET_FORWARD, skb, skb->dev, rth->u.dst.dev,
- // rth->u.dst.input);
- // return NF_STOLEN;
- }
-
- out:
- return ret;;
- }
一.前言
我對linux2.4.18的相關代碼進行了閱讀,從關鍵的幾個接口函數入手,進而理清了整個路由部分的主要脈胳,理解了一些細節問題的處理,但還是有些問題還沒有完全搞清楚。
路由部分代碼主要在linux代碼樹的/net/ipv4/下面:
對於下面報告內容的組織,我想通過由整體到部分到細節的順序,儘量把我閱讀代碼學習到的所有的知識做一個有層次有條理的總結。
二.概述
1. 基於策略的路由
目前在計算機網絡中使用的傳統路由算法都是根據IP包目的地址進行路由選擇.然而在現實應用中經常有這樣的需求:進行路由選擇時不僅僅根據數據報的目的地址,而且根據數據報的其他一些特性如:源地址、IP協議、傳輸層端口,甚至是數據包的負載部分內容,這種類型的路由選擇被稱作基於策略的路由。
2. 路由策略數據庫
在Linux中,從2.1版本的內核開始就實現了對基於策略的路由的支持,它是通過使用路由策略數據庫(RPDB,routing policy database)替代傳統的、基於目的地址的路由表來實現的。RPDB通過包含的一些規則和多張路由表來選定合適的IP路由。這些規則可能會包含很多各種不同類型的鍵值(key),因此這些規則沒有默認的特定次序,規則查找次序或規則優先級都是由網絡或系統管理員設定的。
如下圖所示,Linux的RPDB是一個由數字優先級值進行排序的線性規則列表。RPDB能匹配數據報源地址、目的地址、TOS、進入接口和fwmark值等。每個路由策略規則由一個選擇器和一個動作指示組成。RPDB按照優先級遞增的順序被掃描,RPDB包含的每條規則的選擇器被應用於數據報的源地址、目的地址、進入接口、TOS和fwmark值。若數據報匹配該規則對應於該規則的動作被執行。若動作成功返回,則規則輸出將是一個有效的路由或是路由查找失敗指示;否則查找RPDB的下一條規則。規則的動作通常是查一張與之對應的路由表,但也可以是如下幾種:REJECT(丟棄),PROHIBIT或決UNRECHABLE(丟棄並向源地址發送ICMP包),NAT(源地址網絡地址轉換)等。路由表項的類型除了表示指出下一跳的相關信息外,還可以表示:BLACKHOLE(丟棄),PROHIBIT或UNREACHABL(丟棄並向源地址發送ICMP包)E,NAT(目的地址網絡地址轉換)等。
由圖中所示,系統默認已經實現了三個策略(本地策略、主策略和默認策略),與之對應的是三張默認路由表(本地路由表、主路由表和默認路由表)
3. 相關代碼
我們主要分析了linux2.4.18內核關於路由部分的代碼,主要如下:
linux/net/ipv4/*
route.c 提供了路由部分給IP層調用的接口。
fib_rules.c 提供對路由策略數據庫的查找接口,並維護策略表。
fib_hash.c 對路由表的查找,維護等。
fib_semantics.c 路由表的語義操作,維護路由項信息表(fib_info_list)。
fib_frontend.c 提供對路由表進行操作的接口。
linux/include/net/*
route.h 路由cache中相關的一些數據結構。
ip_fib.h 定義了路由部分很多重要的結構。
neighbour.h struct neighbour的定義。
dst.h 對路由cache結點中dst_entry結構的定義。
linux/net/core/*
dst.c 對路由cache結點分配等動作。
neighbour.c neighbour結構相關操作。
三.路由部分結構
1. 總體結構
內核路由部分代碼實際上是ipv4協議棧的一部分,它被ip層的代碼所調用。主要的調用時機有兩個:一個是IP包輸出時,需要確定由哪個端口出去;另一個是IP包輸入時,需要確定是發給本機還是選擇一個端口發送出去。
整個路由系統可以分成三部分:IP層調用接口,路由策略數據庫,和前後端接口。
1. IP層調用接口主要是提供一組調用接口給IP層代碼,這些接口最終完成了整個的路由工作。爲了提高效率,這部分代碼維護了一個路由策略數據庫的緩存或者叫cache,這部分代碼另一個主要功能就是維護這個緩存了。這部分的代碼主要在route.c文件中。
2. 路由策略數據庫部分主要包括一個策略庫和多張路由表,還有一些相關的操作它們的函數。當路由緩存沒有命中的情況下,就要由這部分完成最後的查找工作。這部分的代碼主要在fib_rules.c,fib_hash.c,fib_semantics.c中。
3. 前底端接口部分主要是給用戶提供的一些對路由策略數據庫增刪改的操作函數,對/proc接口的實現,以及一些定時器的操作。這部分代碼主要在fib_frontend.c中,還有一些分散在其它文件中。
2. IP接口部分結構
這部分即是route.c的內容,主要定義了路由cache還有提供給IP層調用的幾個接口函數。
我們先來介紹一下路由cache的定義:
static struct rt_hash_bucket *rt_hash_table;
這個rt_hash_table即是路由cache,它是一個rt_hash_bucket結構:
struct rt_hash_bucket {
struct rtable *chain;
rwlock_t lock;
};
struct rtable的結構定義如下:
struct rtable
{
union
{
struct dst_entry dst;
struct rtable *rt_next;
} u;
unsigned rt_flags;
unsigned rt_type;
__u32 rt_dst;
__u32 rt_src;
int rt_iif;
__u32 rt_gateway;
struct rt_key key;
__u32 rt_spec_dst;
struct inet_peer *peer;
#ifdef CONFIG_IP_ROUTE_NAT
__u32 rt_src_map;
__u32 rt_dst_map;
#endif
};
struct rt_key
{
__u32 dst;
__u32 src;
int iif;
int oif;
__u8 tos;
__u8 scope;
};
實際上這個rt_hash_table就是一張hash table。每個hash值相同的結點都掛在一個list上即struct rt_hash_bucket的chain成員,它的lock成員用於對這個list進行加鎖以實現臨界資源的互斥訪問。
每個結點是一個rtable結構,這個結構比較重要,實際上路由查詢的最終結果就是把一個對應的rtable結構的地址賦給skb->dst。這個結構的域key就是hash表檢索時所用來比較的關鍵字,這個結構包含了dst(目標地址),src(源地址),iif(入端口),oif(出端口),tos(服務類型),scope(目標地址的範圍),這些也就是查找路由緩存時所要匹配的值,即如果這些都匹配了,那麼說明cache命中,否則還要繼續檢索。
下面這個圖顯示了路由緩存的完整結構:
這一部分主要提供了兩個供IP層調用的入口函數:
int ip_route_input( struct sk_buff* skb, u32 dst, u32src, u8 tos,
struct net_device *dev );
int ip_route_output( struct rtable **rp, u32 daddr, u32 saddr,
u32 tos, int oif )
其中ip_route_input函數即是在處理從網絡上進來的IP包時調用的路由函數,它的結果主要有兩個:即如果是本地包則傳給上層協議層,如果不是則選則一個出端口再發送出去。函數的參數有5個:skb表示ip包的緩衝區,dst目的地址,src源地址,tos表示IP包服務類型,dev表示入端口。函數返回值指示錯誤,如果成功查到路由,函數返回後,skb->dst會被賦值。
與之相對ip_route_output函數則是處理本機發出的IP包時調用的路由函數,它的結果只是爲其選擇一個下一跳以及出端口。參數也是5個:rp是個輸出參數,返回時*rp指向一個返回的rtable結構的路由結點;daddr目的地址,saddr源地址,tos服務類型,oif出接口。函數返回值指示錯誤。
這一部分其它一些比較重要的函數有:
ip_route_input_slow:當ip_route_input查cache不命中時調用此函數,此函數進而調用路由策略數據庫的查詢接口進行查詢,然後更新路由cache。
ip_route_output_slow:當ip_route_output查cahe不命中是調用此函數,此函數進而調用路由策略數據庫的查詢接口進行查詢,然後更新路由cache。
rt_intern_hash:將新rtable結構的結點插入到路由緩存中。
rt_garbage_collect:對路由緩存進和垃圾收集。
3. 路由策略數據庫部分結構
這一部分主要包括策略表及路由表的定義,以及查詢等操作。
① 策略表
static struct fib_rule *fib_rules = &local_rule;
fib_rules即是策略表,它是一個fib_rule結構:
struct fib_rule {
struct fib_rule *r_next;
atomic_t r_clntref;
u32 r_preference;
unsigned char r_table;
unsigned char r_action;
unsigned char r_dst_len;
unsigned char r_src_len;
u32 r_src;
u32 r_srcmask;
u32 r_dst;
u32 r_dstmask;
u32 r_srcmap;
u8 r_flags;
u8 r_tos;
int r_ifindex;
char r_ifname[IFNAMSIZ];
int r_dead;
};
整個策略表的結構如下圖:
這個策略表實際上就是一個單鏈表,整個單鏈表按策略的優先級由高到低的順序排列,表頭指針即是fib_rule。每個策略是一個fib_rule結構。這個結構有幾個重要的域:
r_preference 這個策略的優先級。
r_table 這個策略對應的路由表,它是路由表索引表fib_tables的一個索引值。
r_action 策略的動作,如單播,丟棄,NAT等。
r_src,r_srcmask,r_dst,r_dstmask,r_tos等 策略的選擇器,即描述什麼樣的IP包匹配這條策略。
系統默認已經定義了三個策略:
static struct fib_ruledefault_rule = {
r_clntref: ATOMIC_INIT(2),
r_preference: 0x7FFF,
r_table: RT_TABLE_DEFAULT,
r_action: RTN_UNICAST,
};
static struct fib_rulemain_rule = {
r_next: &default_rule,
r_clntref: ATOMIC_INIT(2),
r_preference: 0x7FFE,
r_table: RT_TABLE_MAIN,
r_action: RTN_UNICAST,
};
static struct fib_rulelocal_rule = {
r_next: &main_rule,
r_clntref: ATOMIC_INIT(2),
r_table: RT_TABLE_LOCAL,
r_action: RTN_UNICAST,
};
可以看到這三個策略(本地策略,主策略,默認策略)按照優先級的由高到低的次序排列,它們的選擇器都是0,即表示匹配所有類型的IP包。它們的動作都是單播就表示都是查對應的路由表。它們分別對應三張路由表(本地路由表,主路由表,默認路由表)。其意義就是對於一個IP包,系統總是按本地路由表->主路由表->默認路由表的次序進行查找的。
② 路由表
定義如下:
struct fib_table *local_table;
struct fib_table *main_table;
struct fib_table *fib_tables[RT_TABLE_MAX+1];
它的數據結構是:
struct fib_table
{
unsigned char tb_id;
unsigned tb_stamp;
int (*tb_lookup)(struct fib_table *tb, const struct rt_key *key, struct fib_result *res);
int (*tb_insert)(…);
int (*tb_delete)(…);
int (*tb_dump)(…);
int (*tb_flush)(struct fib_table *table);
int (*tb_get_info)(…);
void (*tb_select_default)(…);
unsigned char tb_data[0];
};
fib_table[]是系統中所有路由表的索引數組。系統另外定義了兩個路由表指針local_table和main_table,分別指向默認定義的兩個路由表。在前面我們曾介紹系統定義了三張路由表,還有一張即是默認路由表,實際上它只是一張空表,一般並沒有用到。
實際上,fib_table結構只是一個路由表結構中最上層的一個結構,它下面還很多的層次,下面這張圖描繪了整個路由表的數據結構:
整個結構看起來比較複雜,我們可以把它分成4個層次來看:
第一個層次是fib_table和fn_hash結構。實際上,fn_hash結構即是fib_table的tb_data域。這一層主要是包括一個路由表所對應的標識符(tb_id),操作函數指針(tb_looup等),以及對所有路由項的一個總索引(fn_hash結構)。最爲重要的就是這個索引,一個路由表把它所有的路由項劃分成33個區域,劃分的原則即是子網掩碼的長度(從0到32),這33個區域分別對應着fn_hash結構中的fz_zone[0]到fz_zone[32]。之所以這麼劃分的原因就因爲,路由的表的查找要從最精確到最不精確,也就是說要從掩碼最長的路由項查起。
第二個層次是fn_zone結構。每個fn_zone代表了一個區域,由於並不是33個區域都會同時存在,一般往往只有常用到的掩碼長度(如0,16,24,32位)對應的區域才存在,所以所有存在的區域按從大到小的順序被鏈成一個list,從而提高查找的效率。這人fn_zone結構中最重要的就是fz_hash域了,它指向了一個hash table,這個hash table組織了這個區域下的所有路由項。
第三個層次是代表路由項的fn_node結構。它是hash table的結點,其中fn_key域即是hash查找的關鍵字,它實際上就是路由項的目標網絡號。這個結構的提供了路由查找的結果信息,fn_type這個域指示了這個路由項的含義:單播轉發,本地,丟棄,NAT等等。對於大多數情況,路由項都是單播轉發類型的,這時關於下一跳的信息就入在fn_info中了,它指向一個fib_info結構。
第四個層次即是fib_info結構。因爲很多的路由項具有相同的下一跳信息,即fn_node與fib_info結構是多對一的關係。所以fn_node中只存放一個指向fib_info的指針fn_info。所有fib_info結構被單獨管理,它們被組織成一個雙向鏈表,表頭爲fib_info_list。關於下一跳的具體信息由fib_nh[]數組指示,它是一個數組意味着一個下一跳決策可以對應着多個物理的下一跳,這是linux支持的一個MULITPATH功能。
③ 處理函數
這部分的處理函數中最爲重要的就是對路由策略數據庫的查找函數fib_lookup,以及對單個路由表進行查找的fn_hash_lookup函數。
fib_lookup的定義:
int fib_lookup(const struct rt_key *key, struct fib_result *res)
這個函數的工作就是對整個路由策略數據庫進行查找,它會在需要的時候調用fn_hash_lookup查找特定的路由表。函數有兩個參數,key是查找的關鍵字,它與路由緩存查找時的key是一致的。res是輸出參數,函數返回後如果成功則在res存入查找結果。函數的返回值用來指示錯誤。
static int fn_hash_lookup(struct fib_table *tb, const struct rt_key *key, struct fib_result *res)
這個函數的即是對路由進行查找。參數有3個,tb指示待查的路由表,key查找關鍵字,res指向的結構存放查找的結果。函數返回值指示錯誤。
4. 接口部分結構
這一部分主要實現以下幾個功能:
1.對路由表,策略表進行增加項,刪除項,創建表,表空路由緩存等操作。
2.爲路由策略數據庫,路由緩存提供/proc接口。
3.設置定時器,以定時對路由緩存進行清理工作。
四.主要路由流程分析
前面已經介紹過,IP層會在輸入和輸出兩個時候去調用路由部分代碼。輸入路由過程更爲複雜一些也更具代表性,所以我們下面主要分析一下IP包輸入時的路由流程。
下圖描述了這個流程:
當有數據到達網絡設備的時候,會產生一箇中斷,中斷處理函數會調用驅動層的net_rx函數,net_rx進而產生個軟中斷進入net_rx_action函數,進而如是發現這個數據幀是IP包的話,它就調用IP協議層的ip_rcv函數,它進而又調用ip_rcv_finish函數。在這個函數,它調用路由代碼的IP接口函數ip_route_input進行路由。可以看到傳遞給路由代碼的參數有5個:skb IP包緩衝區,iph->daddr IP包的目的地址,iph->saddr IP包源地址,iph->tos 服務類型,dev 輸入的網絡設備。當這個ip_route_input函數返回時,就意味着路由工作已經結束,如果返回值是0,那麼就說明已經成功找到了路由。那麼這個路由查詢結果放在哪裏呢?它就在skb->dst,它指向的就是查到的路由緩存中的一個結點。下邊通過調用skb->dst->input(skb)就可以對這個IP進行處理了。這個input是路由緩存結點中的一個函數指針,如果這個路由項表示轉發的,那麼這個指針實際上指向的是ip_local_deliver,而如果是傳送給本地的,那麼指向的是ip_forward。ip_local_deliver會將這個IP包進一步傳給上層協議層處理,ip_forward則會再將這個IP包從網絡設備發送出去。
我們再來看一下路由的具體流程。
首先調用的是ip_route_input,它的任務主要是查路由緩存,如果找到了那麼它給skb->dst賦值並返回,如是沒找到,它會調用ip_route_input_slow去查詢路由策略數據庫。
下面是經過簡化的代碼和註釋:
int ip_route_input(struct sk_buff *skb, u32 daddr, u32 saddr, u8 tos, struct net_device *dev)
{
int iif = dev->ifindex;
hash = rt_hash_code(daddr, saddr ^ (iif << 5), tos);
/* 遍歷hash table */
for (rth = rt_hash_table[hash].chain; rth; rth = rth->u.rt_next ) {
/* 只有這五個量都匹配纔算命中,要比較這麼多量是因爲在基於策略的路由中,有一個量不同就有可能選擇不同的策略。 */
if ( rth->key.dst == daddr && rth->key.src == saddr &&
rth->key.iif == iif && rth->key.oif == 0 &&
rth->key.tos == tos ) {
rth->u.dst.lastuse = jiffies;
dst_hold(&rth->u.dst);
rth->u.dst.__use++;
/* 關鍵的一步,爲dst爲賦值 */
skb->dst = (struct dst_entry*)rth;
return 0;
}
}
/* 如果緩存查不到,那麼調用這個函數 */
return ip_route_input_slow(skb, daddr, saddr, tos, dev);
}
ip_route_input_slow函數的主要任務是去調用路由策略數據庫的查找函數fib_lookup進行查找,然後更新路由緩存。
因爲這個函數很長,我們用下面的流程圖來表示一些主要的流程:
當調用過fib_lookup後,函數會根據查找的結構進行不同的處理。一般情況是轉發或者本地,這兩種的情況都會先分配一個新的路由緩存結點,填充適當的值然後插入到緩存中;兩者的不同主要在於,設置dst.input函數分別爲ip_forward或ip_local_deliver,轉發的情況還要綁定關於下一跳信息的neighbour(這個結構主要用來得到網段上鄰居的物理地址)。除了轉發或本地還有可能是其它情況,比如有錯誤,沒查到,丟棄,NAT等。
fib_lookup函數是路由策略數據庫的查詢接口,它首先查找策略表,找到一條匹配的策略,然後再執行該策略所對應的動作,動作一般來說就是要查找對應的一張路由表,所以接下來會調用fn_hash_lookup函數進行處理。
下面是這個函數的簡化後的代碼和相關注釋:
fib_lookup(const struct rt_key *key, struct fib_result *res)
{
/* 循環遍歷策略表 */
for (r = fib_rules; r; r=r->r_next) {
/* 如果有一項不符,繼續查找下一個 */
if ( ((saddr^r->r_src) & r->r_srcmask) ||
((daddr^r->r_dst) & r->r_dstmask) ||
(r->r_tos && r->r_tos != key->tos) ||
(r->r_ifindex && r->r_ifindex != key->iif) )
continue;
/* 判斷策略的動作 */
switch (r->r_action) {
case RTN_UNICAST:
case RTN_NAT:
policy = r;
break;
default:
case RTN_BLACKHOLE:
read_unlock(&fib_rules_lock);
return -EINVAL;
}
/* 得到策略所對應的路由表 */
if ((tb = fib_get_table(r->r_table)) == NULL) continue;
/* 查找路由表 */
err = tb->tb_lookup(tb, key, res);
/* 返回0表示查找成功 */
if (err == 0) { res->r = policy; return 0; }
/* 如果有錯誤,則返回錯誤號,如果是-EAGAIN或正數則查下一策略 */
if (err < 0 && err != -EAGAIN) return err;
}
return -ENETUNREACH;
}
fn_hash_lookup函數的主要功能即是對路由表的查找。如下:
int fn_hash_lookup(struct fib_table *tb, const struct rt_key *key, struct fib_result *res)
{
/* 從大到小遍歷區域 */
for (fz = t->fn_zone_list; fz; fz = fz->fz_next) {
fn_key_t k = fz_key(key->dst, fz);
/* 遍歷一區域內的hash table */
for (f = fz_chain(k, fz); f; f = f->fn_next) {
if (!fn_key_eq(k, f->fn_key)) {
if (fn_key_leq(k, f->fn_key)) break;
else continue;
}
/* 找到匹配的路由項 */
if (f->fn_state&FN_S_ZOMBIE) continue;
/* 進行語義上的檢查和設置
如果是單播,把fib_info賦給res
如果是其它,相應作一些處理 */
err = fib_semantic_match(f->fn_type, FIB_INFO(f), key, res);
/* 沒有錯誤的情況 */
if (err == 0) {
res->type = f->fn_type;
res->prefixlen = fz->fz_order;
goto out;
}
if (err < 0) goto out;
}
}
/* 如果沒有找到匹配的路由項,返回正值表示上層函數處理下一個策略 */
err = 1;
out:
return err;
}
五.一些細節問題
1. 關於路由中的錯誤處理
這裏的錯誤是指找不到路由項,還包括丟棄、禁止、不可到達等情況。這些情況產生的原因可能是因爲路由表中找不到相應的項或是用戶設置了相應的策略或路由項對特定IP包進行丟棄等處理。
在這種情況下fib_lookup會返回一個錯誤值,如-ENETUNREACH,-BLACKHOLE等。接着在ip_route_input_slow中
if ((err = fib_lookup(&key, &res)) != 0) {
if (!IN_DEV_FORWARD(in_dev))
goto e_inval;
goto no_route;
}
即會跳到no_route處:
no_route:
rt_cache_stat[smp_processor_id()].in_no_route++;
spec_dst = inet_select_addr(dev, 0, RT_SCOPE_UNIVERSE);
goto local_input;
它把res.type標記成RTN_UNREACHABLE然後跳到本地包情況的處理代碼,先是更新路由緩存,然後遇到如下代碼:
if (res.type == RTN_UNREACHABLE) {
rth->u.dst.input= ip_error;
rth->u.dst.error= -err;
rth->rt_flags &= ~RTCF_LOCAL;
}
rth->rt_type = res.type;
goto intern;
即判斷如果res.type是RTN_UNREACHABLE標記,那麼給函數指針dst.input賦爲ip_err,將dst.error賦爲-err。然後插入到緩存。
最後IP層調用的skb->dst->input實際上就是ip_err(),進行處理錯誤,如發送ICMP包。
2. 策略性路由NAT功能的實現
linux內核的路由機制是可以實現靜態NAT的(即是IP影射是靜態不變的)。其中,源地址的SNAT是通過動作爲NAT的策略來完成的,目的地址的DNAT是通過類型爲NAT的路由項來完成的。
在ip_route_input_slow中,執行完fib_lookup後會有如下代碼:
u32 src_map = saddr;
src_map = fib_rules_policy(saddr, &res, &flags);
key.dst = fib_rules_map_destination(daddr, &res);
fib_res_put(&res);
free_res = 0;
if (fib_lookup(&key, &res))
goto e_inval;
free_res = 1;
goto e_inval;
}
首先,執行fib_rule_policy函數,將判斷如果剛纔查策略表時查到的是動作爲NAT的策略,那麼將策略對應的影射源地址賦給src_map,最後會將這個src_map賦給key.src。這就記錄了SNAT的地址。
然後,if (res.type == RTN_NAT) 判斷查路由表項的類型如果是NAT,那麼將路由表項中的影射目的地址賦給key.dst,這就記錄了DNAT的地址,然後用這個地址再調用fib_lookup函數查一遍影射後的目的地址的路由。
在下面更新緩存的時候有如下代碼:
rth->rt_src_map = key.src;
rth->rt_dst_map = key.dst;
這就把影射後的地址入到了緩存結點中。
進而在執行ip_forward函數進行轉發時,有如下代碼:
if (rt->rt_flags & RTCF_NAT) {
if (ip_do_nat(skb)) {
kfree_skb(skb);
return NET_RX_BAD;
}
}
即如果是NAT,執行ip_do_nat函數做NAT,實際上就是根據skb-dst->rt_src_map和skb-dst->rt_dst_map做地址替換。