天天講路由,那 Linux 路由到底咋實現(xiàn)的?。?/h1>
本文轉載自微信公眾號「開發(fā)內功修煉」,作者張彥飛allen。轉載本文請聯(lián)系開發(fā)內功修煉公眾號。
大家好,我是飛哥。
容器是一種新的虛擬化技術,每一個容器都是一個邏輯上獨立的網(wǎng)絡環(huán)境。Linux 上提供了軟件虛擬出來的二層交換機 Bridge 可以解決同一個宿主機上多個容器之間互連的問題,但這是不夠的。二層交換無法解決容器和宿主機外部網(wǎng)絡的互通。
容器肯定是需要和宿主機以外的外部網(wǎng)絡互通才具備實用價值的。比如在 Kubernets 中,就要求所有的 pod 之間都可以互通。相當于在原先物理機所組成的網(wǎng)絡之上,要再建一個互通的虛擬網(wǎng)絡出來。這就是 Overlay 網(wǎng)絡的概念,用一個簡單的示例圖表示如下。
回想在傳統(tǒng)物理物理網(wǎng)絡中,不同子網(wǎng)之間的服務器是如何互聯(lián)起來的呢,沒錯,就是在三層工作的路由器,也叫網(wǎng)關。路由器使得數(shù)據(jù)包可以從一個子網(wǎng)中傳輸?shù)搅硪粋€子網(wǎng)中,進而實現(xiàn)更大范圍的網(wǎng)絡互通。如下圖所示,一臺路由器將 192.168.0.x 和 192.168.1.x 兩個子網(wǎng)連接了起來。
在容器虛擬化網(wǎng)絡中,自然也需要這么一個角色,將容器和宿主機以外的網(wǎng)絡連接起來。其實 Linux 天生就具備路由的功能,只是在云原生時代,它的路由功能再一次找到了用武之地。在容器和外部網(wǎng)絡通信的過程中,Linux 就又承擔起路由器的角色,實現(xiàn)容器數(shù)據(jù)包的正確轉發(fā)和投遞。
在各種基于容器的云原生技術盛行的今天,再次回頭深刻理解路由工作原理顯得非常有必要,而且也非常的有價值。今天,我們就再來強化一下 Linux 上的路由知識!
一、什么時候需要路由
先來聊聊 Linux 在什么情況下需要路由過程。其實在發(fā)送數(shù)據(jù)時和接收數(shù)據(jù)時都會涉及到路由選擇,為什么?我們挨個來看。
1.1 發(fā)送數(shù)據(jù)時選路
Linux 之所以在發(fā)送數(shù)據(jù)包的時候需要進行路由選擇,這是因為服務器上是可能會有多張網(wǎng)卡設備存在的。數(shù)據(jù)包在發(fā)送的時候,一路通過用戶態(tài)、TCP 層到了 IP 層的時候,就要進行路由選擇,以決定使用哪張網(wǎng)卡設備把數(shù)據(jù)包送出去。詳細過程參見25 張圖,一萬字,拆解 Linux 網(wǎng)絡包發(fā)送過程
來大致過一下路由相關源碼源碼。網(wǎng)絡層發(fā)送的入口函數(shù)是 ip_queue_xmit。
- //file: net/ipv4/ip_output.c
- int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
- {
- // 路由選擇過程
- // 選擇完后記錄路由信息到 skb 上
- rt = (struct rtable *)__sk_dst_check(sk, 0);
- if (rt == NULL) {
- // 沒有緩存則查找路由項
- rt = ip_route_output_ports(...);
- sk_setup_caps(sk, &rt->dst);
- }
- skb_dst_set_noref(skb, &rt->dst);
- ...
- //發(fā)送
- ip_local_out(skb);
- }
在 ip_queue_xmit 里我們開頭就看到了路由項查找, ip_route_output_ports 這個函數(shù)中完成路由選擇。路由選擇就是到路由表中進行匹配,然后決定使用哪個網(wǎng)卡發(fā)送出去。
Linux 中最多可以有 255 張路由表,其中默認情況下有 local 和 main 兩張。使用 ip 命令可以查看路由表的具體配置。拿 local 路由表來舉例。
- #ip route list table local
- local 10.143.x.y dev eth0 proto kernel scope host src 10.143.x.y
- local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
1.2 接收數(shù)據(jù)時選路
沒錯,接收數(shù)據(jù)包的時候也需要進行路由選擇。這是因為 Linux 可能會像路由器一樣工作,將收到的數(shù)據(jù)包通過合適的網(wǎng)卡將其轉發(fā)出去。
Linux 在 IP 層的接收入口 ip_rcv 執(zhí)行后調用到 ip_rcv_finish。在這里展開路由選擇。如果發(fā)現(xiàn)確實就是本設備的網(wǎng)絡包,那么就通過 ip_local_deliver 送到更上層的 TCP 層進行處理。
如果路由后發(fā)現(xiàn)非本設備的網(wǎng)絡包,那就進入到 ip_forward 進行轉發(fā),最后通過 ip_output 發(fā)送出去。
具體的代碼如下。
- //file: net/ipv4/ip_input.c
- static int ip_rcv_finish(struct sk_buff *skb){
- ...
- if (!skb_dst(skb)) {
- int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
- iph->tos, skb->dev);
- ...
- }
- ...
- return dst_input(skb);
- }
其中 ip_route_input_noref 就是在進行路由查找。
- //file: net/ipv4/route.c
- int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,
- u8 tos, struct net_device *dev)
- {
- ...
- res = ip_route_input_slow(skb, daddr, saddr, tos, dev);
- return res;
- }
這里記著 ip_route_input_slow 就行了,后面我們再看。
1.3 linux 路由小結
路由在內核協(xié)議棧中的位置可以用如下一張圖來表示。
網(wǎng)絡包在發(fā)送的時候,需要從本機的多個網(wǎng)卡設備中選擇一個合適的發(fā)送出去。網(wǎng)絡包在接收的時候,也需要進行路由選擇,如果是屬于本設備的包就往上層送到網(wǎng)絡層、傳輸層直到 socket 的接收緩存區(qū)中。如果不是本設備上的包,就選擇合適的設備將其轉發(fā)出去。
二、Linux 的路由實現(xiàn)
2.1 路由表
路由表(routing table)在內核源碼中的另外一個叫法是轉發(fā)信息庫(Forwarding Information Base,F(xiàn)IB)。所以你在源碼中看到的 fib 開頭的定義基本上就是和路由表相關的功能。
其中路由表本身是用 struct fib_table 來表示的。
- struct fib_table {
- struct hlist_node tb_hlist;
- u32 tb_id;
- int tb_default;
- int tb_num_default;
- unsigned long tb_data[0];
- };
所有的路由表都通過一個 hash - fib_table_hash 來組織和管理。它是放在網(wǎng)絡命名空間 net 下的。這也就說明每個命名空間都有自己獨立的路由表。
- //file:include/net/net_namespace.h
- struct net {
- struct netns_ipv4 ipv4;
- ...
- }
- //file: include/net/netns/ipv4.h
- struct netns_ipv4 {
- // 所有路由表
- struct hlist_head *fib_table_hash;
- // netfilter
- ...
- }
在默認情況下,Linux 只有 local 和 main 兩個路由表。如果內核編譯時支持策略路由,那么管理員最多可以配置 255 個獨立的路由表。
如果你的服務器上創(chuàng)建了多個網(wǎng)絡命名空間的話,那么就會存在多套路由表。以除了默認命名網(wǎng)絡空間外,又創(chuàng)了了一個新網(wǎng)絡命名空間的情況為例,路由表在整個內核數(shù)據(jù)結構中的關聯(lián)關系總結如下圖所示。
2.2 路由查找
在上面的小節(jié)中我們看到,發(fā)送過程調用 ip_route_output_ports 來查找路由,接收過程調用 ip_route_input_slow 來查找。但其實這兩個函數(shù)都又最終會調用到 fib_lookup 這個核心函數(shù),源碼如下。
- //file: net/ipv4/route.c
- struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4)
- {
- ...
- // 進入 fib_lookup
- if (fib_lookup(net, fl4, &res)) {
- }
- }
- //file: net/ipv4/route.c
- static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
- u8 tos, struct net_device *dev)
- {
- ...
- // 進入 fib_lookup
- err = fib_lookup(net, &fl4, &res);
- }
我們來看下 fib_loopup 都干了啥。為了容易理解,我們只看一下不支持多路由表版本的 fib_lookup。
- //file: include/net/ip_fib.h
- static inline int fib_lookup(struct net *net, const struct flowi4 *flp,
- struct fib_result *res)
- {
- struct fib_table *table;
- table = fib_get_table(net, RT_TABLE_LOCAL);
- if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))
- return 0;
- table = fib_get_table(net, RT_TABLE_MAIN);
- if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))
- return 0;
- return -ENETUNREACH;
- }
這個函數(shù)就是依次到 local 和 main 表中進行匹配,匹配到后就返回,不會繼續(xù)往下匹配。從上面可以看到 local 表的優(yōu)先級要高于 main 表,如果 local 表中找到了規(guī)則,則路由過程就結束了。
這也就是很多同學說為什么 ping 本機的時候在 eth0 上抓不到包的根本原因。所有命中 local 表的包都會被送往 loopback 設置,不會過 eth0。
三、路由的使用方法
3.1 開啟轉發(fā)路由
在默認情況下,Linux 上的轉發(fā)功能是關閉的,這時候 Linux 發(fā)現(xiàn)收到的網(wǎng)絡包不屬于自己就會將其丟棄。
但在某些場景下,例如對于容器網(wǎng)絡來說,Linux 需要轉發(fā)本機上其它網(wǎng)絡命名空間中過來的數(shù)據(jù)包,需要手工開啟轉發(fā)。如下這兩種方法都可以。
- # sysctl -w net.ipv4.ip_forward=1
- # sysctl net.ipv4.conf.all.forwarding=1
開啟后,Linux 就能像路由器一樣對不屬于本機(嚴格地說是本網(wǎng)絡命名空間)的 IP 數(shù)據(jù)包進行路由轉發(fā)了。
3.2 查看路由表
在默認情況下,Linux 只有 local 和 main 兩個路由表。如果內核編譯時支持策略路由,那么管理員最多可以配置 255 個獨立的路由表。在 centos 上可以通過以下方式查看是否開啟了 CONFIG_IP_MULTIPLE_TABLES 多路由表支持。
- # cat /boot/config-3.10.0-693.el7.x86_64
- CONFIG_IP_MULTIPLE_TABLES=y
- ...
所有的路由表按照從 0 - 255 進行編號,每個編號都有一個別名。編號和別名的對應關系在 /etc/iproute2/rt_tables 這個文件里可以查到。
- # cat /etc/iproute2/rt_tables
- 255 local
- 254 main
- 253 default
- 0 unspec
- 200 eth0_table
查看某個路由表的配置,通過使用 ip route list table {表名} 來查看。
- #ip route list table local
- local 10.143.x.y dev eth0 proto kernel scope host src 10.143.x.y
- local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
如果是查看 main 路由表,也可以直接使用 route 命令
- # route -n
- Kernel IP routing table
- Destination Gateway Genmask Flags Metric Ref Use Iface
- 10.0.0.0 10.*.*.254 255.0.0.0 UG 0 0 0 eth0
- 10.*.*.0 0.0.0.0 255.255.248.0 U 0 0 0 eth0
上面字段中的含義如下
- Destination:目的地址,可以是一個具體的 IP,也可以是一個網(wǎng)段,和 Genmask 一起表示。
- Gateway:網(wǎng)關地址,如果是 0.0.0.0 表示不需要經(jīng)過網(wǎng)關。
- Flags: U 表示有效,G 表示連接路由,H 這條規(guī)則是主機路由,而不是網(wǎng)絡路由。
- Iface:網(wǎng)卡設備,使用哪個網(wǎng)卡將包送過去。
上述結果中輸出的第一條路由規(guī)則表示這臺機器下,確切地說這個網(wǎng)絡環(huán)境下,所有目標為 10.0.0.0/8(Genmask 255.0.0.0 表示前 8 位為子網(wǎng)掩碼) 網(wǎng)段的網(wǎng)絡包都要通過 eth0 設備送到 10...254 這個網(wǎng)關,由它再幫助轉發(fā)。
第二條路由規(guī)則表示,如果目的地址是 10...0/21(Genmask 255.255.248.0 表示前 21 位為子網(wǎng)掩碼)則直接通過 eth0 發(fā)出即可,不需要經(jīng)過網(wǎng)關就可通信。
3.3 修改路由表
默認的 local 路由表是內核根據(jù)當前機器的網(wǎng)卡設備配置自動生成的,不需要手工維護。對于main 的路由表配置我們一般只需要使用 route add 命令就可以了,刪除使用 route del。
修改主機路由
- # route add -host 192.168.0.100 dev eth0 //直連不用網(wǎng)關
- # route add -host 192.168.1.100 dev eth0 gw 192.168.0.254 //下一跳網(wǎng)關
修改網(wǎng)絡路由
- # route add -net 192.168.1.0/24 dev eth0 //直連不用網(wǎng)關
- # route add -net 192.168.1.0/24 dev eth0 gw 10.162.132.110 //下一跳網(wǎng)關
也可以指定一條默認規(guī)則,不命中其它規(guī)則的時候會執(zhí)行到這條。
- # route add default gw 192.168.0.1 eth0
對于其它編號的路由表想要修改的話,就需要使用 ip route 命令了。這里不過多展開,只用 main 表舉一個例子,有更多使用需求的同學請自行搜索。
- # ip route add 192.168.5.0/24 via 10.*.*.110 dev eth0 table main
3.4 路由規(guī)則測試
在配置了一系列路由規(guī)則后,為了快速校驗是否符合預期,可以通過 ip route get 命令來確認。
- # ip route get 192.168.2.25
- 192.168.2.25 via 10.*.*.110 dev eth0 src 10.*.*.161
- cache
本文總結
在現(xiàn)如今各種網(wǎng)絡虛擬化技術里,到處都能看著對路由功能的靈活應用。所以我們今天專門深入研究了一下 Linux 路由工作原理。
在 Linux 內核中,對于發(fā)送過程和接收過程都會涉及路由選擇,其中接收過程的路由選擇是為了判斷是該本地接收還是將它轉發(fā)出去。
默認有 local 和 main 兩個路由表,不過如果安裝的 linux 開啟了 CONFIG_IP_MULTIPLE_TABLES 選項的話,最多能支持 255 張路由表。
路由選擇過程其實不復雜,就是根據(jù)各個路由表的配置找到合適的網(wǎng)卡設備,以及下一跳的地址,然后把包轉發(fā)出去就算是完事。
通過合適地配置路由規(guī)則,容器中的網(wǎng)絡環(huán)境和外部的通信不再是難事。通過大量地干預路由規(guī)則就可以實現(xiàn)虛擬網(wǎng)絡互通。
好了,今天的分享就到這里了,期待你的點贊、再看和轉發(fā)~~