計算機之間需要網絡連接才能進行相互通信,這句話對容器實例依然成立,因此網卡分為物理網卡和虛擬網卡。雖然說這兩種類型的網卡都提供網絡連接的能力,但是虛擬網卡并不直接等同于物理網卡,讀者可以把虛擬網卡看成宿主機或者hypervisor(虛擬機監視器)提供的一種類型的虛擬設備,為虛擬機提供網絡連接的能力。
網卡(network interfaces)在通信通信之前需要初始化,比如配置IP地址等,一張網卡可以分別配置一個IPV4地址和IPV6地址,當然也可以在一張網卡上配置多個IP地址。Linux操作系統也提供了網絡接口(network interface)的概念,并且可以是物理接口(比如以太網卡Ethernet和端口)或者虛擬網絡接口。比如我們在ubuntu操作系統上運行ifconfig,這個命令的輸出結果就是這臺機器上配置的所有物理網絡接口以及配置信息。
# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>? mtu 1500
? ? ? ? inet 172.17.0.2? netmask 255.255.0.0? broadcast 172.17.255.255
? ? ? ? ether 02:42:ac:11:00:02? txqueuelen 0? (Ethernet)
? ? ? ? RX packets 177? bytes 206828 (206.8 KB)
? ? ? ? RX errors 0? dropped 0? overruns 0? frame 0
? ? ? ? TX packets 143? bytes 7966 (7.9 KB)
? ? ? ? TX errors 0? dropped 0 overruns 0? carrier 0? collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING>? mtu 65536
? ? ? ? inet 127.0.0.1? netmask 255.0.0.0
? ? ? ? loop? txqueuelen 1000? (Local Loopback)
? ? ? ? RX packets 0? bytes 0 (0.0 B)
? ? ? ? RX errors 0? dropped 0? overruns 0? frame 0
? ? ? ? TX packets 0? bytes 0 (0.0 B)
? ? ? ? TX errors 0? dropped 0 overruns 0? carrier 0? collisions 0
從ifconfig命令的輸出信息可以看到筆者的這臺機器有兩個網絡接口(network interfaces),lo接口也稱作是loopback interface,回路接口。這是一種特殊的網絡接口,專門用于相同機器上的不同進程之間通信。127.0.0.1是標準的回路接口ip地址,進程發往回路接口的數據包(packet)不會拉開當前機器,只有運行在相同機器上的進程才能接收發給回路地址的數據包。
輸出信息中還包含另外一個網絡接口eth0,通過名字也不難猜測出來是這臺ubuntu機器上的以太網接口,其中inet字段為這個接口的ip地址,我們通過輸出的信息還可以看到子網掩碼,廣播地址,mac地址等配置信息。由于容器運行時會為每個POD(注意是每個POD,不是容器實例)創建新的虛擬網絡接口,因此如果我們在一臺運行著數據眾多容器實例的機器上運行ifconfig,輸入結果會非常長。
除了傳統意義上我們都熟悉的以太網接口和回路接口之外,Linux操作系統還提供Bridge接口,也叫“網橋接口”。允許系統管理員在宿主機上創建多個L2層的網絡,換句話說就是網橋接口如同網橋一樣,為同一臺機器上的多個網絡接口提供相互鏈接的能力。Bridge接口讓POD可以通過各自虛擬的網絡接口相互鏈接,并且通過宿主機來訪問更大范圍的網絡服務。網橋接口的具體工作模式如下圖所示:
如上圖所示,veth設備一般被稱作本地Ethernet通道,經常成對出現,從pod一端看到的就是eth0以太網接口(咱們上邊ifconfg輸出中的eth0就是veth設備),發送到veth設備一端的數據,會馬上出現在設備的另外一端。我們可以通過命令行工具brctl或者ip來管理veth設備以及配置工作。Kubernetes的網絡實現中大量使用了網橋接口,來通過veth設備將不同命名空間的POD進行連接,咱們會在后續的文章中詳細介紹。
對于Linux操作系統來說,數據的收發全靠內核提供的網絡協議棧能力,接下來咱們首先看看內核是如何處理連接的,因為服務路由,防火墻以及很多關鍵組件和功能都依賴于Linux底層的連接和數據包處理能力。
Linux操作系統從2.3版本引入了Netfiler組件,這是內核處理數據包的核心組件。簡單來說Netfiler就提供給用戶空間應用程序的一組回調函數,應用程序可以通過將自己開發的代碼掛到這些回調函數上,來輔助內核處理收到的數據包。
如果我們開發了一個數據包處理程序,就可以將處理程序注冊到Netfiler回調函數上,這樣當內核收到符合要求的數據包,會調用我們注冊的處理程序,咱們在處理程序中可以基于收到的數據包來決定數據改如何處理,比如drop掉數據包,或者對數據包進行修改,然后回傳給內核繼續后續處理。通過這種方式,開發人員就構建豐富的運行在用戶空間的數據包處理或者統計報告應用程序。在Linux內核中,Netfiler和iptables就如同雙胞胎,一起工作實現很多實用的功能。
具體來說,Netfiler中包含5個回調函數(hook,鉤子函數),詳細的信息如下所示:
- NF_IP_PRE_ROUTING hook,對應的iptables chain為PREROUTING,當數據從外部系統進入主機的時候觸發
- NF_IP_LOCAL_IN hook,對應的iptables chain為INPUT,當數據包的目標IP地址和本機匹配的時候觸發
- NF_IP_FORWARD,對應的iptables chain為NAT,當數據包的源和目標地址都和本機不匹配的時候觸發
- NF_IP_LOCAL_OUT,對應的iptables chain為OUTPUT,當數據包為本機發給外部機器的時候觸發
- NF_IP_POST_ROUTING,對應的iptables chain為POSTROUTING,當任意的數據包離開本機的時候觸發
當數據包(packet)被內核收到,Netfilter會按固定的順序逐個觸發我們在上表每個鉤子上注冊的處理程序,基于筆者過往的經驗,理解Netfiler的這5個hooks是理解Kubernetes中kube-proxy提供的服務能力的基礎,因為kube-proxy依賴于iptables工作,而iptables會直接把chain和Netfiler的hooks函數關聯在一起。
具體來說,Netfiler會在數據包處理的過程中,基于特定的條件觸發hooks鉤子函數,我們可以簡單的將這個處理過程總結如下圖:
從上圖我們可以看到對任意數據包(packet)來說,并不是會調用所有的hooks鉤子函數,比如從本機發給外部系統的數據包,會觸發NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING鉤子函數。決定數據包具體會觸發哪些Netfiler鉤子函數取決于兩個因素:1,數據包的源地址是否是本機;2,數據包的目的地址是否是本機。當進程調用本地的某個服務的時候,也就是進程發送的數據包,源地址和目的地址都是本機,會首先觸發NF_IP_LOCAL_OUT鉤子函數,接著觸發NP_IP_POST_ROUTING函數,然后會重新進入到數據包處理流程,依次觸發NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN鉤子函數。
由于本機發給本機這種情況會造成安全問題,因此我們可能會問是否能偽造一個數據包,源地址和目的地址都是127.0.0.1?其實當Linux操作系統內核收到這樣的數據包的時候,默認行為是過濾掉這樣的數據包,根本就不會進入到Netfiler的處理流程。由于127.0.0.1不是一個有效的互聯網可達地址,因此Linux內核會很容易分辨出來這樣的數據包,并采取行動。我們把這種source IP地址不正常的數據包稱作Martian packet。即便是操作系統允許我們關閉過濾此類數據包的選項,筆者強烈建議大家在自己的生產環境不要做類似的嘗試。以下是場景的四種Netfiler處理數據包的流程:
- 從本機發給本機:NF_IP_LOCAL_OUT,NF_IP_LOCAL_IN
- 從本地發給外部機器:NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING
- 外部機器發給本機:NF_IP_PRE_ROUTING,NF_IP_LOCAL_IN
- 外部機器發給外部機器:NF_IP_PRE_ROUTING,NF_IP_FORWARD,NF_IP_POST_ROUTING
另外大家熟知的NAT(網絡地址轉換,Network Address Translation)只會影響在NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT鉤子函數中的路由計算,這是iptables設計的核心,也就是說SNAT和DNAT只會作用域特定的hooks和chains。
對于工具開發人員來說,可以通過調用NF_REGISTER_NET_HOOK方法來注冊數據包處理程序到Netfiler的鉤子函數上,當適配的數據包被收到后,內核會調用注冊的鉤子函數按照特定的業務邏輯來處理。大家應該不難猜到iptables背后的工作原理就是按這種方式。不過對于大部分同學來說,應該這輩子都不會有機會編寫生產ready的數據包處理代碼。
Netfiler會基于鉤子函數返回的結果來處理數據包,具體有一下幾種類型的actions:
- Accept,繼續數據包的處理
- Drop,丟棄數據包
- Queue,將數據包發給用戶空間的處理程序
- Stolen,停止內核態后續數據包處理邏輯,將控制權轉交給用戶空間程序
- Repeat,重新處理數據包
注:筆者要特別強調的是,鉤子函數處理的過程中,是可以改變數據包的狀態,比如調整數據包的TTL字段,masquerade數據包等。
接下來我們討論Netfiler的Conntrack模塊,Conntrack組件主要用來跟蹤Linux操作系統機器上外部連入以及內部連出的連接狀態(connection state)。Conntrack會將受到的每個數據包和特定的連接關聯起來,如果沒有Conntrack提供的連接追蹤能力,內核收到的數據流會顯得非常混亂,Conntrack是操作系統的防火墻和NAT功能模塊的基石。
具體來說,Conntrack模塊可以讓我們實現只允許存在連接(connection)的數據包進入到系統,其他的數據包(任意的數據包)都被丟棄。比如我們可以配置用戶中心的服務只能訪問(outbound)阿里的企業也釘釘用戶信息獲取接口,而不允許外部系統發起到這個服務的連接(inbound)。
對于iptables來說,NAT功能依賴于Conntrack來實現。我們都知道NAT有兩種類型:SNAT(也稱作source NAT,iptables會重寫數據包的源地址),DNAT(也稱作destination NAT,iptables會重新數據包的目的地址)。NAT的使用場景非常廣泛,不光在Kubernetes的Service路由場景中,可能存在于家家戶戶的路由器配置中。舉個例子,咱們家里上網的路由器就采用了SNAT和DNAT來將局域網中192.168地址SNAT成路由器的地址,這樣才能訪問外部的資源并返回。當收到返回的數據包后,有需要使用DNAT來講數據發送給訪問設備,比如家里臥室的臺式機。
正是有了Conntrack提供的連接追蹤功能,數據包就可以自動和所屬的連接建立關系,可以讓我們實現consitent路由決策,比如講某個用戶的連接固定的指向某個后臺機器來處理。Conntrack通過我們熟悉的五元組(源IP地址,源端口號,目標IP地址,目標端口號和L4層的協議)來標識連接,其中端口號用來進行路由計算,端口號用來進行數據包和應用程序的映射(端口號是傳輸層確定數據接受進程的信息)。Conntrack的五元組中需要L4層協議的原因是因為應用程序支持TCP和UDP兩種傳輸層的協議。Conntrack把每個五元組確定的連接稱作flow(流),每個連接除了包含五元組等連接的元數據之外,還包括連接的狀態。
從實現層面看,Conntrack的通過哈希表管理所有的連接,如下圖所示:
如上圖所示,哈希表的key是五元組,而key的空間可以配置。較大的key空間會占用更多的內存,但處理速度會更快。另外操作系統也支持最大連接數的配置(maximum number of flow),因此當Conntrack中所有的連接都被使用完之后,系統就無法再為新的用戶請求服務,因為客戶端無法建立連接。這是不是聽起來像DOS攻擊? 沒錯,通過大量短生命周期的連接迅速耗盡Conntrack的連接數空間是一種常用的DOS攻擊手段。
注:為了內容的完整性,讀者可以通過/proc/sys/net/nf_conntrack_max來修改Conntrack最大的連接數,以及/sys/module/nf_conntrack/parameters/hashsize來調整哈希表的大小,但是筆者強力不建議這么干,除非你知道自己在干什么。
咱們前邊說過Conntrack除了包含五元組信息外,還包含連接的狀態。具體來說Conntrack有四種狀態,不過Conntrack工作在L3層(網絡層),和我們數字的L4層(傳輸層)協議的狀態是兩碼事,詳細信息介紹如下:
- 狀態NEW,連接上發送過或者收到過數據包,但是未看到響應數據包,比如只收到TCP SYN數據包
- 狀態ESTABLISHED,連接上有雙向的數據包,比如收到了TCP SYN數據包,也同時發送了SYN ACK數據包
- 狀態RELATED,建立額外的連接,并通過元數據關聯到原始的連接上。比如FTP應用,FTP客戶端和服務器在22號端口上建立連接,然后會新開連接來實際傳輸數據
- 狀態INVALID,數據包無效,比如收到TCP RST數據包,但是沒有連接信息,也就是Conntrack找不到五元組和這個RST數據包匹配
雖然說Conntrack是內核的模塊,但不見得在我們的系統上被激活,我們可以通過命令lsmod | grep nf_conntrack 來檢查系統上的Conntrack模塊是否被激活,如果沒有的話,可以執行命令sudo modprobe nf_conntrack來加載Conntrack內核模塊。
在Linux數據包處理機制(上)這篇文章的末尾,咱們來聊聊數據包的路由機制。讀者首先要明確的是路由選擇在協議棧的L3層(網絡層),內核在處理數據包的時候,需要為數據包確定下一站是哪里,因為在大部分情況下,數據包的目的機器和當前接受機器不在同一個網絡。
舉個例子說明一下,假設我們希望從本機連接到IP地址為1.2.3.4的機器,并且1.2.3.4這個IP地址不在當前網絡。對于處理數據包的機器來說,能夠做的最大努力就是將數據包發送到離目的機器更近的機器上,而如何選擇這個最近的機器需要路由表的支持。
筆者在自己本地的Ubuntu系統上通過route -n命令就可以查看路由表,輸出如下:
# route -n
Kernel IP routing table
Destination? ? Gateway? ? ? ? Genmask? ? ? ? Flags Metric Ref? ? Use Iface
0.0.0.0? ? ? ? 172.17.0.1? ? ? 0.0.0.0? ? ? ? UG? ? 0? ? ? 0? ? ? ? 0 eth0
172.17.0.0? ? ? 0.0.0.0? ? ? ? 255.255.0.0? ? U? ? 0? ? ? 0? ? ? ? 0 eth0
上邊的輸的是典型的路由配置,筆者的ubuntu機器包含兩個路由,到本機的路由172.17.0.0以及默認路由0.0.0.0。大家如果學過計算機網絡,應該知道我們可以有兩種子網的表達方式:1,通過CIDR的方式(比如172.17.0.0/24);2,通過子網掩碼的方式(比如255.255.0.0)。上邊的輸出采用的是子網掩碼的方式。
因此如果回到我們的問題,本地機器需要連接到IP地址為1.2.3.4的遠程機器上,該如何選擇路由表?基于路由表的最小匹配原則,這個數據包會發送到機器172.17.0.1上,因為172.17.0.0并不匹配IP地址1.2.3.4,如果路由表中有兩個路由項都匹配,那么就選擇metric小的那一項(雖然筆者機器上這兩項的值都是0,但是在生產機器上,網絡管理員會維護這些路由信息,因此會更加豐富)。
注:很多CNI插架重度依賴于操作系統的路由表工作,咱們后邊的內容遇到會詳細介紹。
好了,這篇文章的內容就這么多了,咱們基本覆蓋了Linux內核處理數據包的核心概念和流程。接下來(下)篇文章會繼續討論iptables,IPVS和eBPF的工作原理,為我們介紹容器網絡和Kuberntes網絡機制打下基礎,敬請期待!