特洛伊木馬-圖解VXLAN容器網絡通信方案
一篇文章圍繞一張圖,講述一個主題。不過這個主題偏大,我估計需要好幾篇文章才能說得清楚。
云原生的代表技術包括容器、服務網格、微服務、不可變基礎設施和聲明式API。其中K8s是不可變基礎設施的壓艙石。典型的K8s集群由數十個Node, 成百個Pod,上千個Container組成。相互隔離的容器間需要協作才能完成更大規模的應用。而協作就需要網絡通信。
這篇文章我主要通過下面這張全景圖來講述K8s是如何利用VXLAN來實現K8s的容器通信方案的。網絡通信不是量子糾纏,網絡流量是實打實地通過了各個虛擬的、實體的網絡設備,途徑每個設備節點時自然也會受到設備上的路由、iptables等策略控制。
圖:VXLAN容器網絡方案全景圖
K8s的容器通信方案有很多種。譬如flannel實現的host-gw方案、calico基于三層轉發實現的方案以及本文著重講述的flannel.1 VXLAN方案。為什么我要挑flannel.1 VXLAN方案來細聊呢,因為它夠復雜,涉及到了比較多的虛擬網絡設備和組網技術。
這張圖里面涉及到如下幾種網絡設備,有機會我們單獨拿一篇出來過一下這些設備。
- eth: 物理網卡在內核中的表示。它一端連著網絡棧,另一端通過驅動連接著物理網卡。
- veth: virtual eth。它是成對出現的,類似交叉網線連接的一對物理網卡。從網卡一端流出的數據會原樣流入另外一端。每個veth都有自己的MAC地址,也可以給它設置IP地址。
- bridge: bridge的行為類似二層交換機,又翻譯成網橋。可以將veth,tap等虛擬網絡設備連(插)到它上面。如果數據包的目的 MAC 地址為網橋本身,并且網橋設置了 IP 地址的話,那該數據包就會被認為是bridge收到了發往創建網橋那臺主機的數據包,這個數據包將不會轉發到任何設備,而是直接交給上層(三層)協議棧去處理。
- VTEP:VXLAN 網絡的每個邊緣入口上,布置有一個 VTEP(VXLAN Tunnel Endpoints)設備,它既可以是物理設備,也可以是虛擬化設備,主要負責 VXLAN 協議報文的封包和解包。圖中flannel.1就是一個VTEP設備,它既有IP地址,又有MAC地址。
雖然容器間的網絡方案多種多樣,但所有的容器網絡通信問題,其實都可以歸結為以下幾種場景。本篇我們專注容器間通信的場景,故略去了其它通信主體與容器通信的情形,比如本地Node里面的進程也會和容器通信。留個彩蛋,以后再聊。
- 同一個Pod內的容器間通信。
- 同一個Node內的容器間通信。
- 跨Node的容器間通信。
這里需要強調的一個點是,雖然Pod是K8s編排調度的基本單位,但是通信的需求卻發端于Pod里面的容器。
環境說明
這張圖里面,Node 1 和Node X位于同一個局域網17.168.0.0/24。Node 1的IP地址是17.168.0.2,Node X的IP是17.168.0.3。
K8s集群所使用的子網為10.244.0.0/16。對于網絡17.168.0.0/24和它里面的交換機和路由器來說,K8s集群所使用的子網是無效的網絡,交換機和路由器更是無從轉發、路由任何源IP或目的IP為K8s子網的數據包。
非常明顯的矛盾出現了:K8s集群要通過子網為10.244.0.0/16通信,而宿主機環境卻根本不認識這個子網。我們接下來將看到"特洛伊木馬"的故事在這里再次上演。
我們的目標是在這種矛盾的網絡環境下,解釋清楚pod a里面的container-1訪問pod b里面的container-1時發生了哪些事情。圖中藍色的標線展示了數據流的方向。
圖中的綠色標線和綠色的框圖表示了與VXLAN相關的數據流和網絡封包示意圖。
出于簡單,Node 1里面只畫出了一個Pod, pod a,所有的Pod都連在了bridge cni0上,子網為10.244.0.1/24。Node X里面只畫了兩個Pod, pod b和pod c ,所有的Pod也一樣都連在了bridge cni0上,子網為10.244.1.1/24。
每個Node上面的bridge都分配有IP地址。Pod a的IP地址是10.244.0.2,Pod b的IP地址是10.244.1.3。
同一個Pod內的容器間通信
這是最簡單的情形,內核自帶技能,不需額外的組網技術加持。
需要強調的一個知識點是Pod內部所有的容器是共享同一個網絡棧、routes以及iptables的,因為它們屬于同一個network namespace。
在一個k8s cluster內部,每個Pod擁有獨一無二的IP地址,Pod內部所有的container共享分配Pod的地址。Pod內部的容器共享pod的IP地址,但各個容器的端口不能沖突。
由于Pod調度的原子性,一個Pod內部的所有container只會被調度到一臺主機上運行。類似本地機器上兩個應用程序通過localhost進行進程間通信一樣,同一個Pod內部的容器間可以直接通過localhost來通信。此時的traffic直接通過loopback 網絡設備在兩個容器間流動。圖中的bridge無法感知這樣的traffic,主機上的網絡棧和其它網絡設備更不會感知到。
同一個Node內的容器間通信
圖中Node X上畫出了多個Pod。當Pod b里面的container-1想要訪問Pod c里面的container-1時屬于這個場景。
Pod b里面的路由表決定了訪問Pod c的traffic需要從自己的interface eth0出去。
- src IP:10.244.1.3,dest IP:10.244.1.8。
- src MAC為Pod b veth MAC,dest MAC為Pod c veth MAC。
從圖中可以看到Pod b和Pod c都是插在了bridge上面。作為一個虛擬的二層交換機,它按照二層交換機的行為交換、轉發數據包。
在這種場景下,這兩個container之間的通信行為不會超出bridge的范圍,包括Pod b的container-1通過ARP得知目的container的MAC地址也是在bridge內處理。也不會涉及NAT等地址轉換操作。
跨Node的容器間通信
這是最常用的通信場景。容器訪問api server即是典型的例子。
下面開始最復雜的步驟,這些步驟發生在Node 1。Node X收到以太幀后的操作是一個逆過程,這里不做贅述。
我們按照traffic的流向,以它途徑的各個網絡設備(虛擬的、實體的)為分割節點,分段講述每段發生了什么。
從container到cni0
從Pod a的路由表可知,以太幀需要從它的NIC eth0離開。因為eth0是veth的其中一端,另外一端插在bridge cni0上面,于是以太幀進入cni0。此以太幀的目的MAC地址為bridge。
- src IP:10.244.0.2,dest IP:10.244.1.3。
- src MAC為Pod a veth MAC,dest MAC為cni0 MAC。
從cni0到flannel.1
前面提到該網橋配置有IP地址,現在它收到一個目的MAC地址為自己的數據包,于是觸發了 Linux Bridge 的特殊轉發規則:網橋不會將這個數據包轉發給任何設備,而是直接轉交給主機的三層協議棧處理。
主機協議棧根據host的路由表,從而得知需要把IP包交給本機的flannel.1。
從這步以后就是三層路由了,已經不在網橋的工作范圍之內,而是由 Linux 主機依靠 Netfilter 進行 IP 轉發(IP Forward)去實現的。注意這里是IP包轉發,接收者收到的是3層的package,因而它不包含二層的數據。
flannel.1組裝內部數據幀
至此,越過千山萬水,本機的flannel.1終于收到了IP包。
從這里開始,flannel.1需要想辦法營造幻象:跨主機營造一個虛擬的網絡10.244.0.0/16,好讓Pod a看起來Pod b和它正處于一個完全合法的、信息交換自由無障礙的環境。天真的Pod們完全不知這個網絡是一個虛擬的、私有的、宿主機網絡里面的交換機和路由器根本不認識它這樣一個事實。
前面提到flannel.1收到的是 IP 包,既然是IP包,那它就沒有MAC地址,但flannel.1同時又要想辦法把“原始 IP 包”加上一個目的 MAC 地址(當然也需要包含源flannel.1的MAC地址),封裝成一個完整的二層數據幀,然后發送給位于Node X上的flannel.1。
而大家都知道要組裝一個完整的二層數據幀,首先需要解決的問題是目標 flannel.1的MAC地址是什么呢?下面的提示給出了答案。
Node X上的flannel.1的 MAC 地址是什么?
我們已經知道了Node X上的flannel.1的 IP 地址,它是數據包的目的地。要根據三層 IP 地址查詢對應的二層 MAC 地址,這正是 ARP(Address Resolution Protocol )表的功能。這里要用到的 ARP 記錄,也是 flanneld 進程在 Node 1 節點啟動時,自動添加在 Node 1 上的。我們可以通過 ip 命令看到它,如下所示:
# 在Node 1上
$ ip neigh show dev flannel.1
10.244.1.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT
通過ARP,我們知道了目的 flannel.1的MAC是 5e:f8:4f:00:e3:37。到此時,已經完整地產生了內部數據載荷(Inner payload), 內部IP頭(Inner IP Header) 10.244.1.3和內部Ethernet頭(Inner Ethernet Header)5e:f8:4f:00:e3:37了。
但是,因為上面提到的這些 VTEP 設備的 MAC 地址,對于宿主機網絡來說并沒有什么實際意義,所以上面封裝出來的這個數據幀,并不能在我們的宿主機二層網絡里傳輸。為了方便敘述,我們把它稱為“內部數據幀”(Inner Ethernet Frame),或者叫"原始二層數據幀"(Original Layer 2 Frame)。
封裝好的內部數據幀如全景圖中藍色的方框所示。
接下來,Linux 內核還需要再把“原始二層數據幀”進一步封裝成為宿主機網絡里的一個普通的外部數據幀,好讓它載著“原始二層數據幀”,通過宿主機的 eth0 網卡進行傳輸。
flannel.1組裝VXLAN數據幀
如下圖所示,原始二層數據幀加上VXLAN頭,我們把它叫做“VXLAN數據幀”。在全景圖中,我在藍色的方框上面加了一個灰色的方框,用來表示VXLAN頭。需要特別注意下灰色方框中VNI=1這個部分。VNI(Virtual Network Identifier)長24-bit,在這里flannel.1默認把它設置為1,這樣Node X上面的flannel.1就知道這個數據幀是需要它處理的。
- Flannel 中,VNI 的默認值是 1,這也是宿主機上的 VTEP 設備都叫作 flannel.1 的原因。
有了VXLAN數據幀,就可以開始演繹一個和“特洛伊木馬”相同的故事。VXLAN數據幀如同希臘戰士,但我們的目的不是攻打特洛伊城,而是把這個VXLAN數據幀完整地、神不知鬼不覺地送到城內的flannel.1手里。要達到這個目的,我們還需要一個木馬。
圖:VXLAN數據幀
從flannel.1發起UDP連接
好了,“希臘戰士”有了,我們就差一個木馬了。接下來要做的事情是,像把希臘戰士藏到木馬里一樣,Linux 內核要把這個VXLAN數據幀塞進一個 UDP 包里發出去。上面的全景圖中,我特意把VXLAN數據幀畫得窄了一些,好讓你感覺外圍稍胖的UDP包確實像是個木馬。
Node 1上的flannel.1 設備要扮演一個“網橋”的角色,在二層網絡進行 UDP 包的封包和轉發。在Node 1看來,它會以為自己的 flannel.1 設備只是在向另外一臺宿主機的 flannel.1 設備,發起了一次普通的 UDP 鏈接,卻全然不知它發送的是一個木馬(不要緊張,此木馬非木馬病毒)。
但且慢,先回答一個問題:剛才在組裝內部數據幀的時候,我們知道 flannel.1 設備已經知道了目的 flannel.1 設備的 MAC 地址,但這個 UDP 包該發給哪臺宿主機呢?也就是說,木馬有了,希臘戰士也藏到木馬肚子里了,但特洛伊城在哪里?
是時候輪到一個叫作轉發數據庫(FDB, Forwarding Database)上場幫忙了。這個 flannel.1“網橋”對應的 FDB 信息,也是 flanneld 進程負責維護的。它的內容可以通過 bridge fdb 命令查看到,如下所示:
# 在Node 1上,使用“目的VTEP設備”的MAC地址進行查詢
$ bridge fdb show flannel.1 | grep 5e:f8:4f:00:e3:37
5e:f8:4f:00:e3:37 dev flannel.1 dst 17.168.0.3 self permanent
在上面這條 FDB 記錄里,指定了這樣一條規則:發往我們前面提到的“目的 flannel.1”(MAC 地址是 5e:f8:4f:00:e3:37)的二層數據幀,應該通過本機的flannel.1 設備,發往 IP 地址為 17.168.0.3 的主機。顯然,這臺主機正是 Node X,UDP 包要發往的目的地就找到了。
得到了目的IP地址,自然也會得知Node X的MAC地址。接下來的流程,就是一個正常的,宿主機網絡上的封包工作,且最終從 Node 1 的 eth0 網卡發出去了。只不過這個過程發生在虛擬設備flannel.1上面罷了。