近幾年,Docker、Kubernetes等容器化技術和容器編排工具的興起使技術人員從應用部署和維護的泥淖中解脫出來,同時也改變了很多很多互聯網公司的技術架構。筆者近期也在學習Docker和Kubernetes,對這些新技術所帶來的便捷性和安全性非常著迷,其中尤其對容器化技術的網絡實現方式更為好奇。今天就把近期的對Linux虛擬網絡技術的學習成果分享出來,希望能和大家一起交流學習。
Network Namespace
Network Namespace 是 Linux 內核提供的功能,是實現網絡虛擬化的重要功能,它能創建多個隔離的網絡空間,它們有獨自網絡棧信息。不管是虛擬機還是容器,運行的時候仿佛自己都在獨立的網絡中。而且不同Network Namespace的資源相互不可見,彼此之間無法通信。如下圖所示:
ip netns命令
可以借助ip netns
命令來完成對 Network Namespace 的各種操作。ip netns
命令來自于iproute2
安裝包,一般系統會默認安裝,如果沒有的話,讀者自行安裝。
注意:ip netns
命令修改網絡配置時需要 sudo 權限。
可以通過ip netns
命令完成對Network Namespace 的相關操作,可以通過ip netns help
查看命令幫助信息:
$ ip netns help
Usage: ip netns list
ip netns add NAME
ip netns set NAME NETNSID
ip [-all] netns delete [NAME]
ip netns identify [PID]
ip netns pids NAME
ip [-all] netns exec [NAME] cmd ...
ip netns monitor
ip netns list-id
默認情況下,Linux系統中是沒有任何 Network Namespace的,所以ip netns list
命令不會返回任何信息。
創建Network Namespace
下面,我們通過命令創建一個名為ns0
的命名空間:
$ ip netns add ns0
$ ip netns list
ns0
新創建的 Network Namespace 會出現在/var/run/netns/
目錄下。如果相同名字的 namespace 已經存在,命令會報Cannot create namespace file "/var/run/netns/ns0": File exists
的錯誤。
對于每個 Network Namespace 來說,它會有自己獨立的網卡、路由表、ARP 表、iptables 等和網絡相關的資源。
操作Network Namespace
ip
命令提供了ip netns exec
子命令可以在對應的 Network Namespace 中執行命令。
- 查看新創建 Network Namespace 的網卡信息
$ ip netns exec ns0 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
可以看到,新創建的Network Namespace中會默認創建一個lo
回環網卡,此時網卡處于關閉
狀態。此時,嘗試去 ping 該lo
回環網卡,會提示Network is unreachable
$ ip netns exec ns0 ping 127.0.0.1
connect: Network is unreachable
通過下面的命令啟用lo
回環網卡:
ip netns exec ns0 ip link set lo up
然后再次嘗試去 ping 該lo
回環網卡:
$ ip netns exec ns0 ping -c 3 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.048 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.031 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.029 ms
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.029/0.036/0.048/0.008 ms
轉移設備
我們可以在不同的 Network Namespace 之間轉移設備(如veth)。由于一個設備只能屬于一個 Network Namespace ,所以轉移后在這個 Network Namespace 內就看不到這個設備了。
其中,veth設備屬于可轉移設備,而很多其它設備(如lo、vxlan、ppp、bridge等)是不可以轉移的。
veth pair
veth pair 全稱是 Virtual Ethernet Pair,是一個成對的端口,所有從這對端口一 端進入的數據包都將從另一端出來,反之也是一樣。
引入veth pair是為了在不同的 Network Namespace 直接進行通信,利用它可以直接將兩個 Network Namespace 連接起來。
整個veth的實現非常簡單,有興趣的讀者可以參考源代碼drivers/net/veth.c
的實現。
創建veth pair
$ sudo ip link add type veth
$ ip addr
61: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether e6:39:e1:e0:3a:a0 brd ff:ff:ff:ff:ff:ff
62: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether be:41:49:42:23:6a brd ff:ff:ff:ff:ff:ff
可以看到,此時系統中新增了一對veth pair,將veth0和veth1兩個虛擬網卡連接了起來,此時這對 veth pair 處于”未啟用“狀態。
如果我們想指定 veth pair 兩個端點的名稱,可以使用下面的命令:
ip link add vethfoo type veth peer name vethbar
實現Network Namespace間通信
下面我們利用veth pair實現兩個不同的 Network Namespace 之間的通信。剛才我們已經創建了一個名為ns0
的 Network Namespace,下面再創建一個信息Network Namespace,命名為ns1
$ ip netns add ns1
$ ip netns list
ns1
ns0
然后我們將veth0加入到ns0,將veth1加入到ns1,如下所示:
$ ip link set veth0 netns ns0
$ ip link set veth1 netns ns1
然后我們分別為這對veth pair配置上ip地址,并啟用它們:
$ ip netns exec ns0 iplink set veth0 up
$ ip netns exec ns0 ip addr add 10.0.1.1/24 dev veth0
$ ip netns exec ns1 iplink set veth1 up
$ ip netns exec ns1 ip addr add 10.0.1.2/24 dev veth1
查看這對veth pair的狀態
$ ip netns exec ns0 ip addr
61: veth0@if62: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether e6:39:e1:e0:3a:a0 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet 10.0.1.1/24 scope global veth0
valid_lft forever preferred_lft forever
inet6 fe80::e439:e1ff:fee0:3aa0/64 scope link
valid_lft forever preferred_lft forever
$ ip netns exec ns1 ip addr
62: veth1@if61: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether be:41:49:42:23:6a brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.1.2/24 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::bc41:49ff:fe42:236a/64 scope link
valid_lft forever preferred_lft forever
從上面可以看出,我們已經成功啟用了這個veth pair,并為每個veth設備分配了對應的ip地址。我們嘗試在ns1
中訪問ns0
中的ip地址:
$ ip netns exec ns1 ping -c 3 10.0.1.1
sudo: unable to resolve host zormshogu
PING 10.0.1.1 (10.0.1.1) 56(84) bytes of data.
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.091 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=0.035 ms
64 bytes from 10.0.1.1: icmp_seq=3 ttl=64 time=0.037 ms
--- 10.0.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.035/0.054/0.091/0.026 ms
可以看到,veth pair成功實現了兩個不同Network Namespace之間的網絡交互。
veth查看對端
一旦將veth pair的peer段放入另一個Network Namespace,我們在當前Namespace中就看不到它了。那么,我們怎么才能知道這個veth pair的對端在哪里呢?
可以通過ethtool
工具來查看(當Network Namespace很多時,操作會比較麻煩):
$ ip netns exec ns1 ethtool -S veth1
NIC statistics:
peer_ifindex: 5
得知另一端的接口設備序列號是5,我們再到另一個命名空間中查看序列號5代表什么設備:
$ ip netns exec ns0 ip link | grep 5
veth0
網橋
veth pair打破了 Network Namespace 的限制,實現了不同 Network Namespace 之間的通信。但veth pair有一個明顯的缺陷,就是只能實現兩個網絡接口之間的通信。
如果我們想實現多個網絡接口之間的通信,就可以使用下面介紹的網橋(Bridge)技術。
簡單來說,網橋就是把一臺機器上的若干個網絡接口“連接”起來。其結果是,其中一個網口收到的報文會被復制給其他網口并發送出去。以使得網口之間的報文能夠互相轉發。
網橋的工作原理
網橋對報文的轉發基于MAC地址。網橋能夠解析收發的報文,讀取目標MAC地址的信息,和自己記錄的MAC表結合,來決策報文的轉發目標網口。
為了實現這些功能,網橋會學習源MAC地址,在轉發報文時,網橋只需要向特定的網口進行轉發,從而避免不必要的網絡交互。
如果它遇到一個自己從未學習到的地址,就無法知道這個報文應該向哪個網口轉發,就將報文廣播給所有的網口(報文來源的網口除外)。
網橋的實現
Linux內核是通過一個虛擬的網橋設備(Net Device)來實現橋接的。這個虛擬設備可以綁定若干個以太網接口設備,從而將它們橋接起來。如下圖所示:
如上圖所示,網橋設備 br0 綁定了 eth0 和 eth1。
對于網絡協議棧的上層來說,只看得到 br0,上層協議棧需要發送的報文被送到 br0,網橋設備的處理代碼判斷報文該被轉發到 eth0 還是 eth1,或者兩者皆轉發;反過來,從eth0 或 eth1 接收到的報文被提交給網橋的處理代碼,在這里會判斷報文應該被轉發、丟棄還是提交到協議棧上層。
而有時eth0、eth1 也可能會作為報文的源地址或目的地址,直接參與報文的發送與接收,從而繞過網橋。
brctl
和網橋有關的操作可以使用命令 brctl,這個命令來自 bridge-utils 這個包。
- 創建網橋
# 創建網橋
brctl addbr br0
- 刪除網橋
# 刪除網橋
brctl delbr br0
- 綁定網口
建立一個邏輯網段之后,我們還需要為這個網段分配特定的端口。在Linux 中,一個端口實際上就是一個物理或虛擬網卡。而每個網卡的名稱則分別為eth0 ,eth1 ,eth2 。我們需要把每個網卡一一和br0 這個網段聯系起來,作為br0 中的一個端口。
# 讓eth0 成為br0 的一個端口
brctl addif br0 eth0
# 讓eth1 成為br0 的一個端口
brctl addif br0 eth1
# 讓eth2 成為br0 的一個端口
brctl addif br0 eth2
iptables/netfilter
iptables是Linux實現的軟件防火墻,用戶可以通過iptables設置請求準入和拒絕規則,從而保護系統的安全。
我們也可以把iptables理解成一個客戶端代理,用戶通過iptables這個代理,將用戶安全設定執行到對應的安全框架中,這個“安全框架”才是真正的防火墻,這個框架的名字叫netfilter
。
iptables其實是一個命令行工具,位于用戶空間。
iptables/netfilter(以下簡稱iptables)組成了Linux平臺下的包過濾防火墻,可以完成封包過濾、封包重定向和網絡地址轉換(NAT)等功能。
消息處理鏈
iptables不僅要處理本機接收到的消息,也要處理本機發出的消息。這些消息需要經過一系列的”關卡“才能被本機應用層接收,或者從本機發出,每個”關卡“擔負著不同的工作。這里的”關卡“被稱為”鏈“。
-
INPUT
:進來的數據包應用此規則鏈中的策規則; -
OUTPUT
:外出的數據包應用此規則鏈中的規則; -
FORWARD
:轉發數據包時應用此規則鏈中的規則; -
PREROUTING
:對數據包作路由選擇前應用此鏈中的規則(所有的數據包進來的時侯都先由這個鏈處理); -
POSTROUTING
:對數據包作路由選擇后應用此鏈中的規則(所有的數據包出來的時侯都先由這個鏈處理);
數據包經過各個鏈的處理過程大致如下圖所示:
規則表
從上面我們知道,iptables是按照規則來辦事的,這些規則就是網絡管理員預定義的條件。規則一般的定義為:如果數據包頭符合這樣的條件,就這樣處理“。這些規則并不是嚴格按照添加順序排列在一張規則表中,而是按照功能進行分類,存儲在不同的表中,每個表存儲一類規則:
- Filter
主要用來過濾數據,用來控制讓哪些數據可以通過,哪些數據不能通過,它是最常用的表。 - NAT
用來處理網絡地址轉換的,控制要不要進行地址轉換,以及怎樣修改源地址或目的地址,從而影響數據包的路由,達到連通的目的。 - Mangle
主要用來修改IP數據包頭,比如修改TTL值,同時也用于給數據包添加一些標記,從而便于后續其它模塊對數據包進行處理(這里的添加標記是指往內核skb結構中添加標記,而不是往真正的IP數據包上加東西)。 - Raw
在Netfilter里面有一個叫做鏈接跟蹤的功能,主要用來追蹤所有的連接,而raw表里的rule的功能是給數據包打標記,從而控制哪些數據包不做鏈接跟蹤處理,從而提高性能;優先級最高
。
表和鏈的關系
表和鏈共同完成了iptables對數據包的處理。但并不是每個鏈都包含所有類型的表,所以,有些鏈是天生不具備某些功能的。就像我們去車站乘車的時候,”關卡A“只負責檢查身份證,”B關卡”只負責檢查行李,而“C關卡”功能比較齊全,即負責檢查身份證,又負責檢查行李。二者的關系如下圖所示:
總結
今天我們共同學習了一些常見的Linux虛擬網絡技術。其中,Linux通過Network Namespace實現了網絡的隔離,使網絡協議棧之間互不干擾;并通過veth pair和網橋實現了相同主機上多個Network Namespace之間的數據通信;iptables則可以幫助我們實現網絡安全和數據包的路由轉發功能,從而使主機和主機、容器與容器、容器和宿主機之間可以相互收發消息。在這些技術的共同協作下,才有了現在安全、穩定的虛擬網絡。