手工模擬實(shí)現(xiàn) Docker 容器網(wǎng)絡(luò)!
大家好,我是飛哥!
如今服務(wù)器虛擬化技術(shù)已經(jīng)發(fā)展到了深水區(qū)。現(xiàn)在業(yè)界已經(jīng)有很多公司都遷移到容器上了。我們的開(kāi)發(fā)寫(xiě)出來(lái)的代碼大概率是要運(yùn)行在容器上的。因此深刻理解容器網(wǎng)絡(luò)的工作原理非常的重要。這有這樣將來(lái)遇到問(wèn)題的時(shí)候才知道該如何下手處理。
網(wǎng)絡(luò)虛擬化,其實(shí)用一句話來(lái)概括就是用軟件來(lái)模擬實(shí)現(xiàn)真實(shí)的物理網(wǎng)絡(luò)連接。比如 Docker 就是用純軟件的方式在宿主機(jī)上模擬出來(lái)的獨(dú)立網(wǎng)絡(luò)環(huán)境。我們今天來(lái)徒手打造一個(gè)虛擬網(wǎng)絡(luò),實(shí)現(xiàn)在這個(gè)網(wǎng)絡(luò)里訪問(wèn)外網(wǎng)資源,同時(shí)監(jiān)聽(tīng)端口提供對(duì)外服務(wù)的功能。
看完這一篇后,相信你對(duì) Docker 虛擬網(wǎng)絡(luò)能有進(jìn)一步的理解。好了,我們開(kāi)始!
一、基礎(chǔ)知識(shí)回顧
1.1 veth、bridge 與 namespace
Linux 下的 veth 是一對(duì)兒虛擬網(wǎng)卡設(shè)備,和我們常見(jiàn)的 lo 很類似。在這兒設(shè)備里,從一端發(fā)送數(shù)據(jù)后,內(nèi)核會(huì)尋找該設(shè)備的另一半,所以在另外一端就能收到。不過(guò) veth 只能解決一對(duì)一通信的問(wèn)題。詳情參見(jiàn)輕松理解 Docker 網(wǎng)絡(luò)虛擬化基礎(chǔ)之 veth 設(shè)備!
如果有很多對(duì)兒 veth 需要互相通信的話,就需要引入 bridge 這個(gè)虛擬交換機(jī)。各個(gè) veth 對(duì)兒可以把一頭連接在 bridge 的接口上,bridge 可以和交換機(jī)一樣在端口之間轉(zhuǎn)發(fā)數(shù)據(jù),使得各個(gè)端口上的 veth 都可以互相通信。參見(jiàn)
Namespace 解決的是隔離性的問(wèn)題。每個(gè)虛擬網(wǎng)卡設(shè)備、進(jìn)程、socket、路由表等等網(wǎng)絡(luò)棧相關(guān)的對(duì)象默認(rèn)都是歸屬在 init_net 這個(gè)缺省的 namespace 中的。不過(guò)我們希望不同的虛擬化環(huán)境之間是隔離的,用 Docker 來(lái)舉例,那就是不能讓 A 容器用到 B 容器的設(shè)備、路由表、socket 等資源,甚至連看一眼都不可以。只有這樣才能保證不同的容器之間復(fù)用資源的同時(shí),還不會(huì)影響其它容器的正常運(yùn)行。參見(jiàn)
通過(guò) veth、namespace 和 bridge 我們?cè)谝慌_(tái) Linux 上就能虛擬多個(gè)網(wǎng)絡(luò)環(huán)境出來(lái)。而且它們之間、和宿主機(jī)之間都可以互相通信。
但是這三篇文章過(guò)后,我們還剩下一個(gè)問(wèn)題沒(méi)有解決,那就是虛擬出來(lái)的網(wǎng)絡(luò)環(huán)境和外部網(wǎng)絡(luò)的通信。還拿 Docker 容器來(lái)舉例,你啟動(dòng)的容器里的服務(wù)肯定是需要訪問(wèn)外部的數(shù)據(jù)庫(kù)的。還有就是可能需要暴露比如 80 端口對(duì)外提供服務(wù)。例如在 Docker 中我們通過(guò)下面的命令將容器的 80 端口上的 web 服務(wù)要能被外網(wǎng)訪問(wèn)的到。
我們今天的文章主要就是解決這兩個(gè)問(wèn)題的,一是從虛擬網(wǎng)絡(luò)中訪問(wèn)外網(wǎng),二是在虛擬網(wǎng)絡(luò)中提供服務(wù)供外網(wǎng)使用。解決它們需要用到路由和 nat 技術(shù)。
1.2 路由選擇
Linux 是在發(fā)送數(shù)據(jù)包的時(shí)候,會(huì)涉及到路由過(guò)程。這個(gè)發(fā)送數(shù)據(jù)包既包括本機(jī)發(fā)送數(shù)據(jù)包,也包括途徑當(dāng)前機(jī)器的數(shù)據(jù)包的轉(zhuǎn)發(fā)。
先來(lái)看本機(jī)發(fā)送數(shù)據(jù)包。其中本機(jī)發(fā)送在25 張圖,一萬(wàn)字,拆解 Linux 網(wǎng)絡(luò)包發(fā)送過(guò)程這一篇我們討論過(guò)。
所謂路由其實(shí)很簡(jiǎn)單,就是該選擇哪張網(wǎng)卡(虛擬網(wǎng)卡設(shè)備也算)將數(shù)據(jù)寫(xiě)進(jìn)去。到底該選擇哪張網(wǎng)卡呢,規(guī)則都是在路由表中指定的。Linux 中可以有多張路由表,最重要和常用的是 local 和 main。
local 路由表中統(tǒng)一記錄本地,確切的說(shuō)是本網(wǎng)絡(luò)命名空間中的網(wǎng)卡設(shè)備 IP 的路由規(guī)則。
- #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
其它的路由規(guī)則,一般都是在 main 路由表中記錄著的。可以用 ip route list table local 查看,也可以用更簡(jiǎn)短的 route -n
再看途徑當(dāng)前機(jī)器的數(shù)據(jù)包的轉(zhuǎn)發(fā)。除了本機(jī)發(fā)送以外,轉(zhuǎn)發(fā)也會(huì)涉及路由過(guò)程。如果 Linux 收到數(shù)據(jù)包以后發(fā)現(xiàn)目的地址并不是本地的地址的話,就可以選擇把這個(gè)數(shù)據(jù)包從自己的某個(gè)網(wǎng)卡設(shè)備上轉(zhuǎn)發(fā)出去。這個(gè)時(shí)候和本機(jī)發(fā)送一樣,也需要讀取路由表。根據(jù)路由表的配置來(lái)選擇從哪個(gè)設(shè)備將包轉(zhuǎn)走。
不過(guò)值得注意的是,Linux 上轉(zhuǎn)發(fā)功能默認(rèn)是關(guān)閉的。也就是發(fā)現(xiàn)目的地址不是本機(jī) IP 地址默認(rèn)是將包直接丟棄。需要做一些簡(jiǎn)單的配置,然后 Linux 才可以干像路由器一樣的活兒,實(shí)現(xiàn)數(shù)據(jù)包的轉(zhuǎn)發(fā)。
1.3 iptables 與 NAT
Linux 內(nèi)核網(wǎng)絡(luò)棧在運(yùn)行上基本上是一個(gè)純內(nèi)核態(tài)的東西,但為了迎合各種各樣用戶層不同的需求,內(nèi)核開(kāi)放了一些口子出來(lái)供用戶層來(lái)干預(yù)。其中 iptables 就是一個(gè)非常常用的干預(yù)內(nèi)核行為的工具,它在內(nèi)核里埋下了五個(gè)鉤子入口,這就是俗稱的五鏈。
Linux 在接收數(shù)據(jù)的時(shí)候,在 IP 層進(jìn)入 ip_rcv 中處理。再執(zhí)行路由判斷,發(fā)現(xiàn)是本機(jī)的話就進(jìn)入 ip_local_deliver 進(jìn)行本機(jī)接收,最后送往 TCP 協(xié)議層。在這個(gè)過(guò)程中,埋了兩個(gè) HOOK,第一個(gè)是 PRE_ROUTING。這段代碼會(huì)執(zhí)行到 iptables 中 pre_routing 里的各種表。發(fā)現(xiàn)是本地接收后接著又會(huì)執(zhí)行到 LOCAL_IN,這會(huì)執(zhí)行到 iptables 中配置的 input 規(guī)則。
在發(fā)送數(shù)據(jù)的時(shí)候,查找路由表找到出口設(shè)備后,依次通過(guò) __ip_local_out、 ip_output 等函數(shù)將包送到設(shè)備層。在這兩個(gè)函數(shù)中分別過(guò)了 OUTPUT 和 PREROUTING 開(kāi)的各種規(guī)則。
如果是轉(zhuǎn)發(fā)過(guò)程,Linux 收到數(shù)據(jù)包發(fā)現(xiàn)不是本機(jī)的包可以通過(guò)查找自己的路由表找到合適的設(shè)備把它轉(zhuǎn)發(fā)出去。那就先是在 ip_rcv 中將包送到 ip_forward 函數(shù)中處理,最后在 ip_output 函數(shù)中將包轉(zhuǎn)發(fā)出去。在這個(gè)過(guò)程中分別過(guò)了 PREROUTING、FORWARD 和 POSTROUTING 三個(gè)規(guī)則。
綜上所述,iptables 里的五個(gè)鏈在內(nèi)核網(wǎng)絡(luò)模塊中的位置就可以歸納成如下這幅圖。
數(shù)據(jù)接收過(guò)程走的是 1 和 2,發(fā)送過(guò)程走的是 4 、5,轉(zhuǎn)發(fā)過(guò)程是 1、3、5。有了這張圖,我們能更清楚地理解 iptable 和內(nèi)核的關(guān)系。
在 iptables 中,根據(jù)實(shí)現(xiàn)的功能的不同,又分成了四張表。分別是 raw、mangle、nat 和 filter。其中 nat 表實(shí)現(xiàn)我們常說(shuō)的 NAT(Network AddressTranslation) 功能。其中 nat 又分成 SNAT(Source NAT)和 DNAT(Destination NAT)兩種。
SNAT 解決的是內(nèi)網(wǎng)地址訪問(wèn)外部網(wǎng)絡(luò)的問(wèn)題。它是通過(guò)在 POSTROUTING 里修改來(lái)源 IP 來(lái)實(shí)現(xiàn)的。
DNAT 解決的是內(nèi)網(wǎng)的服務(wù)要能夠被外部訪問(wèn)到的問(wèn)題。它在通過(guò) PREROUTING 修改目標(biāo) IP 實(shí)現(xiàn)的。
二、 實(shí)現(xiàn)虛擬網(wǎng)絡(luò)外網(wǎng)通信
基于以上的基礎(chǔ)知識(shí),我們用純手工的方式搭建一個(gè)可以和 Docker 類似的虛擬網(wǎng)絡(luò)。而且要實(shí)現(xiàn)和外網(wǎng)通信的功能。
1. 實(shí)驗(yàn)環(huán)境準(zhǔn)備
我們先來(lái)創(chuàng)建一個(gè)虛擬的網(wǎng)絡(luò)環(huán)境出來(lái),其命名空間為 net1。宿主機(jī)的 IP 是 10.162 的網(wǎng)段,可以訪問(wèn)外部機(jī)器。虛擬網(wǎng)絡(luò)為其分配 192.168.0 的網(wǎng)段,這個(gè)網(wǎng)段是私有的,外部機(jī)器無(wú)法識(shí)別。
這個(gè)虛擬網(wǎng)絡(luò)的搭建過(guò)程如下。先創(chuàng)建一個(gè) netns 出來(lái),命名為 net1。
- # ip netns add net1
創(chuàng)建一個(gè) veth 對(duì)兒(veth1 - veth1_p),把其中的一頭 veth1 放在 net1 中,給它配置上 IP,并把它啟動(dòng)起來(lái)。
- # ip link add veth1 type veth peer name veth1_p
- # ip link set veth1 netns net1
- # ip netns exec net1 ip addr add 192.168.0.2/24 dev veth1 # IP
- # ip netns exec net1 ip link set veth1 up
創(chuàng)建一個(gè) bridge,給它也設(shè)置上 ip。接下來(lái)把 veth 的另外一端 veth1_p 插到 bridge 上面。最后把網(wǎng)橋和 veth1_p 都啟動(dòng)起來(lái)。
- # brctl addbr br0
- # ip addr add 192.168.0.1/24 dev br0
- # ip link set dev veth1_p master br0
- # ip link set veth1_p up
- # ip link set br0 up
這樣我們就在 Linux 上創(chuàng)建出了一個(gè)虛擬的網(wǎng)絡(luò)。創(chuàng)建過(guò)程和 聊聊 Linux 上軟件實(shí)現(xiàn)的“交換機(jī)” - Bridge! 中一樣,只不過(guò)今天為了省事,只創(chuàng)建了一個(gè)網(wǎng)絡(luò)出來(lái),上一篇中創(chuàng)建出來(lái)了兩個(gè)。
2. 請(qǐng)求外網(wǎng)資源
現(xiàn)在假設(shè)我們上面的 net1 這個(gè)網(wǎng)絡(luò)環(huán)境中想訪問(wèn)外網(wǎng)。這里的外網(wǎng)是指的虛擬網(wǎng)絡(luò)宿主機(jī)外部的網(wǎng)絡(luò)。
我們假設(shè)它要訪問(wèn)的另外一臺(tái)機(jī)器 IP 是 10.153.*.* ,這個(gè) 10.153.*.* 后面兩段由于是我的內(nèi)部網(wǎng)絡(luò),所以隱藏起來(lái)了。你在實(shí)驗(yàn)的過(guò)程中,用自己的 IP 代替即可。
我們直接來(lái)訪問(wèn)一下試試
- # ip netns exec net1 ping 10.153.*.*
- connect: Network is unreachable
提示網(wǎng)絡(luò)不通,這是怎么回事?用這段報(bào)錯(cuò)關(guān)鍵字在內(nèi)核源碼里搜索一下:
- //file: arch/parisc/include/uapi/asm/errno.h
- #define ENETUNREACH 229 /* Network is unreachable */
- //file: net/ipv4/ping.c
- static int ping_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
- size_t len)
- {
- ...
- rt = ip_route_output_flow(net, &fl4, sk);
- if (IS_ERR(rt)) {
- err = PTR_ERR(rt);
- rt = NULL;
- if (err == -ENETUNREACH)
- IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);
- goto out;
- }
- ...
- out:
- return err;
- }
在 ip_route_output_flow 這里的返回值判斷如果是 ENETUNREACH 就退出了。這個(gè)宏定義注釋上來(lái)看報(bào)錯(cuò)的信息就是 “Network is unreachable”。
這個(gè) ip_route_output_flow 主要是執(zhí)行路由選路。所以我們推斷可能是路由出問(wèn)題了,看一下這個(gè)命名空間的路由表。
- # ip netns exec net1 route -n
- Kernel IP routing table
- Destination Gateway Genmask Flags Metric Ref Use Iface
- 192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1
怪不得,原來(lái) net1 這個(gè) namespace 下默認(rèn)只有 192.168.0.* 這個(gè)網(wǎng)段的路由規(guī)則。我們 ping 的 IP 是 10.153.*.* ,根據(jù)這個(gè)路由表里找不到出口。自然就發(fā)送失敗了。
我們來(lái)給 net 添加上默認(rèn)路由規(guī)則,只要匹配不到其它規(guī)則就默認(rèn)送到 veth1 上,同時(shí)指定下一條是它所連接的 bridge(192.168.0.1)。
- # ip netns exec net1 route add default gw 192.168.0.1 veth1
再 ping 一下試試。
- # ip netns exec net1 ping 10.153.*.* -c 2
- PING 10.153.*.* (10.153.*.*) 56(84) bytes of data.
- --- 10.153.*.* ping statistics ---
- 2 packets transmitted, 0 received, 100% packet loss, time 999ms
額好吧,仍然不通。上面路由幫我們把數(shù)據(jù)包從 veth 正確送到了 bridge 這個(gè)網(wǎng)橋上。接下來(lái)網(wǎng)橋還需要 bridge 轉(zhuǎn)發(fā)到 eth0 網(wǎng)卡上。所以我們得打開(kāi)下面這兩個(gè)轉(zhuǎn)發(fā)相關(guān)的配置
- # sysctl net.ipv4.conf.all.forwarding=1
- # iptables -P FORWARD ACCEPT
不過(guò)這個(gè)時(shí)候,還存在一個(gè)問(wèn)題。那就是外部的機(jī)器并不認(rèn)識(shí) 192.168.0.* 這個(gè)網(wǎng)段的 ip。它們之間都是通過(guò) 10.153.*.* 來(lái)進(jìn)行通信的。設(shè)想下我們工作中的電腦上沒(méi)有外網(wǎng) IP 的時(shí)候是如何正常上網(wǎng)的呢?外部的網(wǎng)絡(luò)只認(rèn)識(shí)外網(wǎng) IP。沒(méi)錯(cuò),那就是我們上面說(shuō)的 NAT 技術(shù)。
我們這次的需求是實(shí)現(xiàn)內(nèi)部虛擬網(wǎng)絡(luò)訪問(wèn)外網(wǎng),所以需要使用的是 SNAT。它將 namespace 請(qǐng)求中的 IP(192.168.0.2)換成外部網(wǎng)絡(luò)認(rèn)識(shí)的 10.153.*.*,進(jìn)而達(dá)到正常訪問(wèn)外部網(wǎng)絡(luò)的效果。
- # iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -o br0 -j MASQUERADE
來(lái)再 ping 一下試試,歐耶,通了!
- # ip netns exec net1 ping 10.153.*.*
- PING 10.153.*.* (10.153.*.*) 56(84) bytes of data.
- 64 bytes from 10.153.*.*: icmp_seq=1 ttl=57 time=1.70 ms
- 64 bytes from 10.153.*.*: icmp_seq=2 ttl=57 time=1.68 ms
這時(shí)候我們可以開(kāi)啟 tcpdump 抓包查看一下,在 bridge 上抓到的包我們能看到還是原始的源 IP 和 目的 IP。
再到 eth0 上查看的話,源 IP 已經(jīng)被替換成可和外網(wǎng)通信的 eth0 上的 IP 了。
至此,容器就可以通過(guò)宿主機(jī)的網(wǎng)卡來(lái)訪問(wèn)外部網(wǎng)絡(luò)上的資源了。我們來(lái)總結(jié)一下這個(gè)發(fā)送過(guò)程
3. 開(kāi)放容器端口
我們?cè)倏紤]另外一個(gè)需求,那就是把在這個(gè)命名空間內(nèi)的服務(wù)提供給外部網(wǎng)絡(luò)來(lái)使用。
和上面的問(wèn)題一樣,我們的虛擬網(wǎng)絡(luò)環(huán)境中 192.168.0.2 這個(gè) IP 外界是不認(rèn)識(shí)它的。只有這個(gè)宿主機(jī)知道它是誰(shuí)。所以我們同樣還需要 NAT 功能。
這次我們是要實(shí)現(xiàn)外部網(wǎng)絡(luò)訪問(wèn)內(nèi)部地址,所以需要的是 DNAT 配置。DNAT 和 SNAT 配置中有一個(gè)不一樣的地方就是需要明確指定容器中的端口在宿主機(jī)上是對(duì)應(yīng)哪個(gè)。比如在 docker 的使用中,是通過(guò) -p 來(lái)指定端口的對(duì)應(yīng)關(guān)系。
- # docker run -p 8000:80 ...
我們通過(guò)如下這個(gè)命令來(lái)配置 DNAT 規(guī)則
- # iptables -t nat -A PREROUTING ! -i br0 -p tcp -m tcp --dport 8088 -j DNAT --to-destination 192.168.0.2:80
這里表示的是宿主機(jī)在路由之前判斷一下如果流量不是來(lái)自 br0,并且是訪問(wèn) tcp 的 8088 的話,那就轉(zhuǎn)發(fā)到 192.168.0.2:80 。
在 net1 環(huán)境中啟動(dòng)一個(gè) Server
- # ip netns exec net1 nc -lp 80
外部選一個(gè)ip,比如 10.143.*.*, telnet 連一下 10.162.*.* 8088 試試,通了!
- # telnet 10.162.*.* 8088
- Trying 10.162.*.*...
- Connected to 10.162.*.*.
- Escape character is '^]'.
開(kāi)啟抓包, # tcpdump -i eth0 host 10.143.*.*。可見(jiàn)在請(qǐng)求的時(shí)候,目的是宿主機(jī)的 IP 的端口。
但數(shù)據(jù)包到宿主機(jī)協(xié)議棧以后命中了我們配置的 DNAT 規(guī)則,宿主機(jī)把它轉(zhuǎn)發(fā)到了 br0 上。在 bridge 上由于沒(méi)有那么多的網(wǎng)絡(luò)流量包,所以不用過(guò)濾直接抓包就行,# tcpdump -i br0。
在 br0 上抓到的目的 IP 和端口是已經(jīng)替換過(guò)的了。
bridge 當(dāng)然知道 192.168.0.2 是 veth 1。于是,在 veth1 上監(jiān)聽(tīng) 80 的服務(wù)就能收到來(lái)自外界的請(qǐng)求了!我們來(lái)總結(jié)一下這個(gè)接收過(guò)程
三、總結(jié)
現(xiàn)在業(yè)界已經(jīng)有很多公司都遷移到容器上了。我們的開(kāi)發(fā)寫(xiě)出來(lái)的代碼大概率是要運(yùn)行在容器上的。因此深刻理解容器網(wǎng)絡(luò)的工作原理非常的重要。這有這樣將來(lái)遇到問(wèn)題的時(shí)候才知道該如何下手處理。
本文開(kāi)頭我們先是簡(jiǎn)單介紹了 veth、bridge、namespace、路由、iptables 等基礎(chǔ)知識(shí)。Veth 實(shí)現(xiàn)連接,bridge 實(shí)現(xiàn)轉(zhuǎn)發(fā),namespace 實(shí)現(xiàn)隔離,路由表控制發(fā)送時(shí)的設(shè)備選擇,iptables 實(shí)現(xiàn) nat 等功能。
接著基于以上基礎(chǔ)知識(shí),我們采用純手工的方式搭建了一個(gè)虛擬網(wǎng)絡(luò)環(huán)境。
這個(gè)虛擬網(wǎng)絡(luò)可以訪問(wèn)外網(wǎng)資源,也可以提供端口服務(wù)供外網(wǎng)來(lái)調(diào)用。這就是 Docker 容器網(wǎng)絡(luò)工作的基本原理。
整個(gè)實(shí)驗(yàn)我打包寫(xiě)成一個(gè) Makefile,放到了這里:https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test07
最后,我們?cè)贁U(kuò)展一下。今天我們討論的問(wèn)題是 Docker 網(wǎng)絡(luò)通信的問(wèn)題。Docker 容器通過(guò)端口映射的方式提供對(duì)外服務(wù)。外部機(jī)器訪問(wèn)容器服務(wù)的時(shí)候,仍然需要通過(guò)容器的宿主機(jī) IP 來(lái)訪問(wèn)。
在 Kubernets 中,對(duì)跨主網(wǎng)絡(luò)通信有更高的要求,要不同宿主機(jī)之間的容器可以直接互聯(lián)互通。所以 Kubernets 的網(wǎng)絡(luò)模型也更為復(fù)雜。