手工模擬實(shí)現(xiàn) Docker 容器網(wǎng)絡(luò)?。ǔ敿?xì)~)
如今服務(wù)器虛擬化技術(shù)已經(jīng)發(fā)展到了深水區(qū)。現(xiàn)在業(yè)界已經(jīng)有很多公司都遷移到容器上了。我們的開發(fā)寫出來的代碼大概率是要運(yùn)行在容器上的。因此深刻理解容器網(wǎng)絡(luò)的工作原理非常的重要。只有這樣將來遇到問題的時(shí)候才知道該如何下手處理。
網(wǎng)絡(luò)虛擬化,其實(shí)用一句話來概括就是用軟件來模擬實(shí)現(xiàn)真實(shí)的物理網(wǎng)絡(luò)連接。比如 Docker 就是用純軟件的方式在宿主機(jī)上模擬出來的獨(dú)立網(wǎng)絡(luò)環(huán)境。我們今天來徒手打造一個(gè)虛擬網(wǎng)絡(luò),實(shí)現(xiàn)在這個(gè)網(wǎng)絡(luò)里訪問外網(wǎng)資源,同時(shí)監(jiān)聽端口提供對外服務(wù)的功能。

看完這一篇后,相信你對 Docker 虛擬網(wǎng)絡(luò)能有進(jìn)一步的理解。
一、基礎(chǔ)知識回顧
1.1 veth、bridge 與 namespace
Linux 下的 veth 是一對兒虛擬網(wǎng)卡設(shè)備,和我們常見的 lo 很類似。在這兒設(shè)備里,從一端發(fā)送數(shù)據(jù)后,內(nèi)核會尋找該設(shè)備的另一半,所以在另外一端就能收到。不過 veth 只能解決一對一通信的問題。
如果有很多對兒 veth 需要互相通信的話,就需要引入 bridge 這個(gè)虛擬交換機(jī)。各個(gè) veth 對兒可以把一頭連接在 bridge 的接口上,bridge 可以和交換機(jī)一樣在端口之間轉(zhuǎn)發(fā)數(shù)據(jù),使得各個(gè)端口上的 veth 都可以互相通信。參見
Namespace 解決的是隔離性的問題。每個(gè)虛擬網(wǎng)卡設(shè)備、進(jìn)程、socket、路由表等等網(wǎng)絡(luò)棧相關(guān)的對象默認(rèn)都是歸屬在 init_net 這個(gè)缺省的 namespace 中的。不過我們希望不同的虛擬化環(huán)境之間是隔離的,用 Docker 來舉例,那就是不能讓 A 容器用到 B 容器的設(shè)備、路由表、socket 等資源,甚至連看一眼都不可以。只有這樣才能保證不同的容器之間復(fù)用資源的同時(shí),還不會影響其它容器的正常運(yùn)行。參見
通過 veth、namespace 和 bridge 我們在一臺 Linux 上就能虛擬多個(gè)網(wǎng)絡(luò)環(huán)境出來。而且它們之間、和宿主機(jī)之間都可以互相通信。

我們還剩下一個(gè)問題沒有解決,那就是虛擬出來的網(wǎng)絡(luò)環(huán)境和外部網(wǎng)絡(luò)的通信。還拿 Docker 容器來舉例,你啟動(dòng)的容器里的服務(wù)肯定是需要訪問外部的數(shù)據(jù)庫的。還有就是可能需要暴露比如 80 端口對外提供服務(wù)。例如在 Docker 中我們通過下面的命令將容器的 80 端口上的 web 服務(wù)要能被外網(wǎng)訪問的到。
我們今天主要就是解決這兩個(gè)問題的,一是從虛擬網(wǎng)絡(luò)中訪問外網(wǎng),二是在虛擬網(wǎng)絡(luò)中提供服務(wù)供外網(wǎng)使用。解決它們需要用到路由和 nat 技術(shù)。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)??


1.2 路由選擇
Linux 是在發(fā)送數(shù)據(jù)包的時(shí)候,會涉及到路由過程。這個(gè)發(fā)送數(shù)據(jù)包既包括本機(jī)發(fā)送數(shù)據(jù)包,也包括途徑當(dāng)前機(jī)器的數(shù)據(jù)包的轉(zhuǎn)發(fā)。
先來看本機(jī)發(fā)送數(shù)據(jù)包。
所謂路由其實(shí)很簡單,就是該選擇哪張網(wǎng)卡(虛擬網(wǎng)卡設(shè)備也算)將數(shù)據(jù)寫進(jìn)去。到底該選擇哪張網(wǎng)卡呢,規(guī)則都是在路由表中指定的。Linux 中可以有多張路由表,最重要和常用的是 local 和 main。
local 路由表中統(tǒng)一記錄本地,確切的說是本網(wǎng)絡(luò)命名空間中的網(wǎng)卡設(shè)備 IP 的路由規(guī)則。
其它的路由規(guī)則,一般都是在 main 路由表中記錄著的??梢杂?ip route list table local 查看,也可以用更簡短的 route -n

再看途徑當(dāng)前機(jī)器的數(shù)據(jù)包的轉(zhuǎn)發(fā)。除了本機(jī)發(fā)送以外,轉(zhuǎn)發(fā)也會涉及路由過程。如果 Linux 收到數(shù)據(jù)包以后發(fā)現(xiàn)目的地址并不是本地的地址的話,就可以選擇把這個(gè)數(shù)據(jù)包從自己的某個(gè)網(wǎng)卡設(shè)備上轉(zhuǎn)發(fā)出去。這個(gè)時(shí)候和本機(jī)發(fā)送一樣,也需要讀取路由表。根據(jù)路由表的配置來選擇從哪個(gè)設(shè)備將包轉(zhuǎn)走。
不過值得注意的是,Linux 上轉(zhuǎn)發(fā)功能默認(rèn)是關(guān)閉的。也就是發(fā)現(xiàn)目的地址不是本機(jī) IP 地址默認(rè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)核開放了一些口子出來供用戶層來干預(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è)過程中,埋了兩個(gè) HOOK,第一個(gè)是 PRE_ROUTING。這段代碼會執(zhí)行到 iptables 中 pre_routing 里的各種表。發(fā)現(xiàn)是本地接收后接著又會執(zhí)行到 LOCAL_IN,這會執(zhí)行到 iptables 中配置的 input 規(guī)則。
在發(fā)送數(shù)據(jù)的時(shí)候,查找路由表找到出口設(shè)備后,依次通過 __ip_local_out、 ip_output 等函數(shù)將包送到設(shè)備層。在這兩個(gè)函數(shù)中分別過了 OUTPUT 和 POSTROUTING 開的各種規(guī)則。
如果是轉(zhuǎn)發(fā)過程,Linux 收到數(shù)據(jù)包發(fā)現(xiàn)不是本機(jī)的包可以通過查找自己的路由表找到合適的設(shè)備把它轉(zhuǎn)發(fā)出去。那就先是在 ip_rcv 中將包送到 ip_forward 函數(shù)中處理,最后在 ip_output 函數(shù)中將包轉(zhuǎn)發(fā)出去。在這個(gè)過程中分別過了 PREROUTING、FORWARD 和 POSTROUTING 三個(gè)規(guī)則。
綜上所述,iptables 里的五個(gè)鏈在內(nèi)核網(wǎng)絡(luò)模塊中的位置就可以歸納成如下這幅圖。

數(shù)據(jù)接收過程走的是 1 和 2,發(fā)送過程走的是 4 、5,轉(zhuǎn)發(fā)過程是 1、3、5。有了這張圖,我們能更清楚地理解 iptable 和內(nèi)核的關(guān)系。
在 iptables 中,根據(jù)實(shí)現(xiàn)的功能的不同,又分成了四張表。分別是 raw、mangle、nat 和 filter。其中 nat 表實(shí)現(xiàn)我們常說的 NAT(Network AddressTranslation) 功能。其中 nat 又分成 SNAT(Source NAT)和 DNAT(Destination NAT)兩種。
SNAT 解決的是內(nèi)網(wǎng)地址訪問外部網(wǎng)絡(luò)的問題。它是通過在 POSTROUTING 里修改來源 IP 來實(shí)現(xiàn)的。
DNAT 解決的是內(nèi)網(wǎng)的服務(wù)要能夠被外部訪問到的問題。它在通過 PREROUTING 修改目標(biāo) IP 實(shí)現(xiàn)的。
二、 實(shí)現(xiàn)虛擬網(wǎng)絡(luò)外網(wǎng)通信
基于以上的基礎(chǔ)知識,我們用純手工的方式搭建一個(gè)可以和 Docker 類似的虛擬網(wǎng)絡(luò)。而且要實(shí)現(xiàn)和外網(wǎng)通信的功能。
1. 實(shí)驗(yàn)環(huán)境準(zhǔn)備
我們先來創(chuàng)建一個(gè)虛擬的網(wǎng)絡(luò)環(huán)境出來,其命名空間為 net1。宿主機(jī)的 IP 是 10.162 的網(wǎng)段,可以訪問外部機(jī)器。虛擬網(wǎng)絡(luò)為其分配 192.168.0 的網(wǎng)段,這個(gè)網(wǎng)段是私有的,外部機(jī)器無法識別。

這個(gè)虛擬網(wǎng)絡(luò)的搭建過程如下。先創(chuàng)建一個(gè) netns 出來,命名為 net1。
創(chuàng)建一個(gè) veth 對兒(veth1 - veth1_p),把其中的一頭 veth1 放在 net1 中,給它配置上 IP,并把它啟動(dòng)起來。
創(chuàng)建一個(gè) bridge,給它也設(shè)置上 ip。接下來把 veth 的另外一端 veth1_p 插到 bridge 上面。最后把網(wǎng)橋和 veth1_p 都啟動(dòng)起來。
這樣我們就在 Linux 上創(chuàng)建出了一個(gè)虛擬的網(wǎng)絡(luò)。
2. 請求外網(wǎng)資源
現(xiàn)在假設(shè)我們上面的 net1 這個(gè)網(wǎng)絡(luò)環(huán)境中想訪問外網(wǎng)。這里的外網(wǎng)是指的虛擬網(wǎng)絡(luò)宿主機(jī)外部的網(wǎng)絡(luò)。
我們假設(shè)它要訪問的另外一臺機(jī)器 IP 是 10.153.*.* ,這個(gè) 10.153.*.* 后面兩段由于是我的內(nèi)部網(wǎng)絡(luò),所以隱藏起來了。你在實(shí)驗(yàn)的過程中,用自己的 IP 代替即可。

我們直接來訪問一下試試
提示網(wǎng)絡(luò)不通,這是怎么回事?用這段報(bào)錯(cuò)關(guān)鍵字在內(nèi)核源碼里搜索一下:
在 ip_route_output_flow 這里的返回值判斷如果是 ENETUNREACH 就退出了。這個(gè)宏定義注釋上來看報(bào)錯(cuò)的信息就是 “Network is unreachable”。
這個(gè) ip_route_output_flow 主要是執(zhí)行路由選路。所以我們推斷可能是路由出問題了,看一下這個(gè)命名空間的路由表。
怪不得,原來 net1 這個(gè) namespace 下默認(rèn)只有 192.168.0.* 這個(gè)網(wǎng)段的路由規(guī)則。我們 ping 的 IP 是 10.153.*.* ,根據(jù)這個(gè)路由表里找不到出口。自然就發(fā)送失敗了。
我們來給 net 添加上默認(rèn)路由規(guī)則,只要匹配不到其它規(guī)則就默認(rèn)送到 veth1 上,同時(shí)指定下一條是它所連接的 bridge(192.168.0.1)。
再 ping 一下試試。
額好吧,仍然不通。上面路由幫我們把數(shù)據(jù)包從 veth 正確送到了 bridge 這個(gè)網(wǎng)橋上。接下來網(wǎng)橋還需要 bridge 轉(zhuǎn)發(fā)到 eth0 網(wǎng)卡上。所以我們得打開下面這兩個(gè)轉(zhuǎn)發(fā)相關(guān)的配置
不過這個(gè)時(shí)候,還存在一個(gè)問題。那就是外部的機(jī)器并不認(rèn)識 192.168.0.* 這個(gè)網(wǎng)段的 ip。它們之間都是通過 10.153.*.* 來進(jìn)行通信的。設(shè)想下我們工作中的電腦上沒有外網(wǎng) IP 的時(shí)候是如何正常上網(wǎng)的呢?外部的網(wǎng)絡(luò)只認(rèn)識外網(wǎng) IP。沒錯(cuò),那就是我們上面說的 NAT 技術(shù)。
我們這次的需求是實(shí)現(xiàn)內(nèi)部虛擬網(wǎng)絡(luò)訪問外網(wǎng),所以需要使用的是 SNAT。它將 namespace 請求中的 IP(192.168.0.2)換成外部網(wǎng)絡(luò)認(rèn)識的 10.153.*.*,進(jìn)而達(dá)到正常訪問外部網(wǎng)絡(luò)的效果。
來再 ping 一下試試,耶,通了!
這時(shí)候我們可以開啟 tcpdump 抓包查看一下,在 bridge 上抓到的包我們能看到還是原始的源 IP 和 目的 IP。

再到 eth0 上查看的話,源 IP 已經(jīng)被替換成可和外網(wǎng)通信的 eth0 上的 IP 了。

至此,容器就可以通過宿主機(jī)的網(wǎng)卡來訪問外部網(wǎng)絡(luò)上的資源了。我們來總結(jié)一下這個(gè)發(fā)送過程

3. 開放容器端口
我們再考慮另外一個(gè)需求,那就是把在這個(gè)命名空間內(nèi)的服務(wù)提供給外部網(wǎng)絡(luò)來使用。
和上面的問題一樣,我們的虛擬網(wǎng)絡(luò)環(huán)境中 192.168.0.2 這個(gè) IP 外界是不認(rèn)識它的。只有這個(gè)宿主機(jī)知道它是誰。所以我們同樣還需要 NAT 功能。
這次我們是要實(shí)現(xiàn)外部網(wǎng)絡(luò)訪問內(nèi)部地址,所以需要的是 DNAT 配置。DNAT 和 SNAT 配置中有一個(gè)不一樣的地方就是需要明確指定容器中的端口在宿主機(jī)上是對應(yīng)哪個(gè)。比如在 docker 的使用中,是通過 -p 來指定端口的對應(yīng)關(guān)系。
我們通過如下這個(gè)命令來配置 DNAT 規(guī)則
這里表示的是宿主機(jī)在路由之前判斷一下如果流量不是來自 br0,并且是訪問 tcp 的 8088 的話,那就轉(zhuǎn)發(fā)到 192.168.0.2:80 。
在 net1 環(huán)境中啟動(dòng)一個(gè) Server
外部選一個(gè)ip,比如 10.143.*.*, telnet 連一下 10.162.*.* 8088 試試,通了!
開啟抓包, # tcpdump -i eth0 host 10.143.*.*??梢娫谡埱蟮臅r(shí)候,目的是宿主機(jī)的 IP 的端口。

編輯切換為居中
但數(shù)據(jù)包到宿主機(jī)協(xié)議棧以后命中了我們配置的 DNAT 規(guī)則,宿主機(jī)把它轉(zhuǎn)發(fā)到了 br0 上。在 bridge 上由于沒有那么多的網(wǎng)絡(luò)流量包,所以不用過濾直接抓包就行,# tcpdump -i br0。
在 br0 上抓到的目的 IP 和端口是已經(jīng)替換過的了。

bridge 當(dāng)然知道 192.168.0.2 是 veth 1。于是,在 veth1 上監(jiān)聽 80 的服務(wù)就能收到來自外界的請求了!我們來總結(jié)一下這個(gè)接收過程

三、總結(jié)
現(xiàn)在業(yè)界已經(jīng)有很多公司都遷移到容器上了。我們的開發(fā)寫出來的代碼大概率是要運(yùn)行在容器上的。因此深刻理解容器網(wǎng)絡(luò)的工作原理非常的重要。這有這樣將來遇到問題的時(shí)候才知道該如何下手處理。
本文開頭我們先是簡單介紹了 veth、bridge、namespace、路由、iptables 等基礎(chǔ)知識。Veth 實(shí)現(xiàn)連接,bridge 實(shí)現(xiàn)轉(zhuǎn)發(fā),namespace 實(shí)現(xiàn)隔離,路由表控制發(fā)送時(shí)的設(shè)備選擇,iptables 實(shí)現(xiàn) nat 等功能。
接著基于以上基礎(chǔ)知識,我們采用純手工的方式搭建了一個(gè)虛擬網(wǎng)絡(luò)環(huán)境。

這個(gè)虛擬網(wǎng)絡(luò)可以訪問外網(wǎng)資源,也可以提供端口服務(wù)供外網(wǎng)來調(diào)用。這就是 Docker 容器網(wǎng)絡(luò)工作的基本原理。
整個(gè)實(shí)驗(yàn)我打包寫成一個(gè) Makefile,放到了這里:https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test07
最后,我們再擴(kuò)展一下。今天我們討論的問題是 Docker 網(wǎng)絡(luò)通信的問題。Docker 容器通過端口映射的方式提供對外服務(wù)。外部機(jī)器訪問容器服務(wù)的時(shí)候,仍然需要通過容器的宿主機(jī) IP 來訪問。
在 Kubernets 中,對跨主網(wǎng)絡(luò)通信有更高的要求,要不同宿主機(jī)之間的容器可以直接互聯(lián)互通。所以 Kubernets 的網(wǎng)絡(luò)模型也更為復(fù)雜。
