Skip to main content

前言

上篇文章 Docker Network - 網路模型 中跟大家分享了 Docker 內幾種基本網路模型,從 None, Host, Bridge 到 Contianer 共享

而今天這篇文章會繼續往下看,我們的最終目的是想要理解預設情況下,我們的 Container 是如何擁有對外網路存取能力的。

介紹

事實上,上篇文章介紹的那些模型,彼此之間是可以互相轉換的,只要你夠熟悉 Linux 上相關指令的操作與用法,你就有辦法自己打造這些模型,甚至轉換。

今天這篇文章探討如何打造一個 Bridge 的網路模型,最後我們會基於基於該網路模型上使用多個容器,並且確保這些容器彼此可以互相存取

本篇文章中還不會探討到容器是如何對外上網的,譬如存取 Google DNS server,而是先著重於底層的架構,以及容器間同網段的存取。

環境

為了更好展示整個過程的變化,我們會先透過 None 的網路模型創造一個完全乾淨的容器,接下來則會透過 Linux 指令的方式將其改造為 Bridge 網路模型。

最後,我們會根據上述步驟,打造兩個容器並且都採用 Bridge 網路模型,讓這兩個容器可以互通,但是還沒有辦法擁有對外上網的能力。

相關程式碼都可以於這邊找到 technologynoteniu/bloglab-source code

步驟

接下來的步驟會伴隨著跟上次一樣的圖例,從不同角度來慢慢觀察每個步驟對系統產生的變化,同時每個步驟也會附上相關的程式碼,有興趣的人也可以於自己環境中測試。

下述環境基於

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.3 LTS
Release: 18.04
Codename: bionic

$ uname -a
Linux k8s-dev 4.15.0-72-generic #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

$ docker --version
Docker version 19.03.13, build 4484c46d9d

創建兩個 None 容器

首先,我們要先創造兩個乾淨的容器,最後目標是打造成 Bridge 的網路模型並確保這兩個容器能夠互相存取。

這邊會先使用 --network=none 的形式要求 Docker 不要對該網路模型動手腳,讓我們自己處理即可。

這邊我們創造容器的時候,我們會特別給予一個參數 --privileged 來要求特別權限,原因後面會解釋,這邊先直接使用

示範程式碼

透過下列的程式碼,我們會於系統中創造兩個容器,分別名為 c1 及 c2. 之後透過 docker 指令進去看,確認兩個容器內除了 lo 之外沒有其他任何網卡。

$ docker run --privileged -d --network=none --name c1 hwchiu/netutils
$ docker run --privileged -d --network=none --name c2 hwchiu/netutils

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
868f54ee0b32 hwchiu/netutils "/bin/bash ./entrypo…" 11 minutes ago Up 11 minutes c2
5df4ed8e756a hwchiu/netutils "/bin/bash ./entrypo…" 11 minutes ago Up 11 minutes c1

$ docker exec c1 ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

模型分析

這邊用兩張不同視角來看待這個行為,由於本篇文章都會採用相同的概念去解讀不同網路模型,因此這邊詳細介紹兩種視角的概念。

圖左:這邊想要介紹的是以系統底層的角度去觀察網路,中間的灰色線將其分為上半部分的 UserSpace, 以及下半部分的 Kernel Space。 本範例中下圖會有不同顏色變化,代表不同的網路空間,每個網路空間彼此網路隔離。

圖右:這邊提供一個比較簡略的介紹,主要會從使用者的角度去觀察,由圖例來說明網路元件的關係上有什麼變化。


  1. 當我們透過 None 創建兩個容器時,同時也會於系統內創建兩個全新的 netns(Network Namespace),如下圖黃色及淺綠色所述,而淺藍色則是原生宿主機所使用的。
  2. 預設情況下,這些 netns 裡面都只會有 lo 這張預設網卡
  3. 假設 eth0 是宿主機本身擁有的網卡

創建 Linux Bridge

接下來我們先於系統中創建一個 Linux Bridge,這也是 Docker 預設網路模型的作法。

這部份會需要使用 brctl 這個工具, Ubuntu 系統可以透過安裝 brctl-utils 來取得。

範例程式碼

這邊我們會使用兩個 brctl 的指令來處理,分別是

  1. brctl add-br $name
  2. brctl show

第一個指令會於系統中創建一個名為 $name 的 Linux Bridge 第二個指令則是會顯示目前系統中有多少 Linux Bridge,以及其相關資訊

範例中我們會創立一個名為 hwchiu0 的 Linux Bridge,最後透過 ifconfig 這個指令讓 Bridge 給叫起來,讓他處於一個可運行的狀態。

$ sudo brctl addbr hwchiu0
$ sudo brctl show
bridge name bridge id STP enabled interfaces
hwchiu0 8000.000000000000 no
$ sudo ifconfig hwchiu0 up

網路模型

執行完這個階段後,我們系統會於宿主機的 netns (Network Namespace) 中創造一個全新的 Linux Bridge (hwchiu0)。

這時候的系統架構圖如下,基本上跟上述沒有太多變化,單純就是多了一個元件。

創建 Veth Pair

目前我們擁有

  1. 兩個空蕩蕩的容器,包含其本身的 netns
  2. 一個 Linux Bridge

因此接下來我們要做的就是想辦法將其串連起來,這邊我們會使用一個名為 veth 的特殊網路設備,透過該設備我們可以於系統中創造一條特殊的連結。 該連結會有兩個端口,分別都會有對應的網卡名稱,從一端進去的封包都會馬上從另外一端出來,可以想像成一個雙向水管的概念。

因此這個步驟我們要先於宿主機上面創造兩條雙向水管,也就是兩條 veth pair.

程式碼示範

這個範例中,我們需要透過 ip 這個指令來創建該 veth 水管,指令變化很多,這邊示範一種用法

ip link add dev ${name} type veth

上述指令會要求 kernel 幫忙創造一條基於 veth 型態的連結,並且其中一個端頭命名為 ${name},至於另一端則讓 kernel 幫忙處理

當該指令完畢後,系統中就會多出兩張虛擬網卡,分別是 ${name} 以及 kernel 幫忙創造的,通常是 veth....

$ sudo ip link add dev c1-eth0 type veth
$ sudo ip link add dev c2-eth0 type veth
$ sudo ip link | grep veth
23: veth0@c1-eth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
24: c1-eth0@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
25: veth1@c2-eth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
26: c2-eth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000

透過上述的指令,我們創建了兩條基於 veth 的網卡,因此系統中產生了四個虛擬網卡,其配對分別為

  1. veth1 --> c2-eth0
  2. veth0 --> c1-eth0

網路模型

網卡建立完畢後,這時候的網路模型如下

左圖: 當前都是於宿主機內操作,所以四張虛擬網卡都坐落於宿主機的 netns 裡面,圖中用兩種不同顏色的連線來代表這些網卡的關係

右圖: 系統創建完畢後,宿主機多出四張虛擬網卡,這四張虛擬網卡跟容器無關,跟 Linux 無關,可以想成四個孤兒

移動 Veth 到容器

當我們創建好相關的 veth 水管後,下一個步驟就是要將 veth 的一個端頭給放到容器之中,這樣我們就可以利用 veth 的特性於不同 netns 之中傳遞封包。

上述創造的配對如下

  1. veth1 --> c2-eth0
  2. veth0 --> c1-eth0

因此我們的目標就是

  1. c1-eth0 這個虛擬網卡放到 c1 容器
  2. c2-eth0 這個虛擬網卡放到 c2 容器

然而,準確的說,我們其實不是要放到容器內,而是要放到容器所屬的 netns (network namespace) 內,所以我們要先有辦法接觸到這些容器所使用的 netns.

這邊我們會使用 ip netns 指令來進行操作,這個指令預設會去讀取 /var/run/netns 底下的資料來顯示相關的 netns

然而 docker 其本身設計預設則是會避開 /var/run/netns 可能是怕有人直接用系統指令 ip netns 來操作導致容器崩壞。 因此透過 docker 所創造的容器其所屬的 netns 都會放到 /var/run/docker/netns

這邊我們只需要透過一個 soft link 的方式將這兩個位置串接起來,我們就可以使用 ip netns 來觀察 c1,c2 兩個容器的 netns 了

程式碼範例

根據上述概念,我們先執行 ln 指令來創造 soft link,然後透過 ip netns show 來展示出系統當前看到的 netns 名稱

$ sudo ln -s /var/run/docker/netns /var/run/netns
$ sudo ip netns show
792fedcf97d8
1bb2e0141544

這邊的兩個名稱其實是 docker 容器裡面的 NetworkSettings.SnadboxID 我們可以透過下列指令來觀察

$ docker inspect c1 | jq '.[0].NetworkSettings.SandboxID'
"1bb2e0141544758fe79387ebf4b7297556fb65efacc7d9ed7e068099744babee"

$ docker inspect c2 | jq '.[0].NetworkSettings.SandboxID'
"792fedcf97d8ae10ec0a29f5aa41813ad00825ff8127fd4d9c25b66a5714d7ca"

因此當前範例中, 792fedcf97d8 代表是的 c2 容器的 netns,而 1bb2e0141544 則是 c1 容器的 netns。

接下來終於要進入正題,把我們事先創建好的虛擬網卡(veth的一端)給放到對應的 netns 中,這邊要借助 ip link set 這指令,該指令可以把虛擬網卡給放到不同的 netns 內,同時也可以重新命名

$ sudo ip link set c1-eth0 netns 1bb2e0141544 name eth0
$ sudo ip link set c2-eth0 netns 792fedcf97d8 name eth0

執行完畢上述指令後,我們接下來可以透過 docker 指令再次觀察是否有任何變化

$ sudo docker exec c2 ifconfig -a
eth0 Link encap:Ethernet HWaddr ea:51:1c:2c:a4:15
BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
$ sudo docker exec c1 ifconfig -a
eth0 Link encap:Ethernet HWaddr be:a7:29:1b:e0:13
BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
$ ip link | grep veth
23: veth0@if24: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
25: veth1@if26: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000

最後我們可以再次透過 ip link 指令觀察,可以發現宿主機上面的四張虛擬網卡只剩下兩張了,因為有兩張都被搬移到容器內。

網路模型

當相關的虛擬網卡都被移動到對應的容器後,這時候的模型有一點點小小變化

左圖: veth 的一端都被移動到所屬容器的 netns 內,並且重新命名為 eth0

右圖: 這邊的改變就是 veth 的端口被搬移到容器中,並且重新命名為 eth0

veth 綁定 Bridge

接下來,我們要將 veth 跟 Linux Bridge 給整合一起,希望藉由 Linux Bridge 的功能來幫我們轉發封包

所以這邊的概念很簡單,就是把宿主機上面關於 veth 的網卡通通都綁到我們事先創造好的 Linux Bridge hwchiu0 身上

程式碼範例

這邊我們會使用 brctl addif 這個指令來達成目標,該指令用法如下

brctl addif ${bridge_name} ${nic_name}

該指令會將名為 ${nic_name} 的網卡給加入到 ${bridge_name} 的 Linux Bridge 上。

當加入完畢後,我們也順便透過 ifconfig (其實 ip link 也可以) 將 veth 虛擬網卡給叫起來讓其運作

$ sudo brctl addif hwchiu0 veth0
$ sudo brctl addif hwchiu0 veth1
$ sudo ifconfig veth0 up
$ sudo ifconfig veth1 up
$ sudo brctl show
bridge name bridge id STP enabled interfaces
hwchiu0 8000.266248dc8ca1 no veth0
veth1

最後我們可以透過 brctl show 來顯示當前系統上面關於 Linux Bridge 的資訊,可以觀察到 veth0 以及 veth1 都已經綁定上去。

網路模型

這個步驟主要針對的是 veth 於宿主機內的搬移,將其綁到對應的 Linux Bridge 上。

左圖/右圖: 差異就是 veth 的兩個端頭現在都不流浪,而是歸屬於 Linux Bridge 底下

設定 Container IP

現在我們幾乎已經將整個路線給串通了!剩下最後一個簡單步驟就是設定 IP

我們兩個容器內的 eth0 網卡都還沒有 IP,因此這個步驟我們就來幫忙設定 IP, IP 本身議題也很多,這邊我們為了避免更多問題,因此這兩個容器的 IP 我們設定同網段,就如同我們使用 Docker 容器般,大家都是同網段

目標:

  • c1 容器使用 10.55.66.2 的IP, 網段為 10.55.66.0/24
  • c2 容器使用 10.55.66.3 的IP, 網段為 10.55.66.0/24

程式碼範例

還記得一開始創造容器時有特別要求 --privileged 這個參數嗎?

這是因為我們接下來要於容器內透過一些網路工具去修改網卡資訊,這部份我們會需要相關的權限,否則執行的時候會得到下列錯誤

SIOCSIFADDR: Operation not permitted
SIOCSIFFLAGS: Operation not permitted
SIOCSIFNETMASK: Operation not permitted
SIOCSIFFLAGS: Operation not permitted
$ sudo docker exec c1 ifconfig eth0 10.55.66.2 netmask 255.255.255.0 up
$ sudo docker exec c2 ifconfig eth0 10.55.66.3 netmask 255.255.255.0 up
$ sudo docker exec c1 ifconfig
eth0 Link encap:Ethernet HWaddr be:a7:29:1b:e0:13
inet addr:10.55.66.2 Bcast:10.55.66.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:11 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:906 (906.0 B) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

$ sudo docker exec c2 ifconfig
eth0 Link encap:Ethernet HWaddr ea:51:1c:2c:a4:15
inet addr:10.55.66.3 Bcast:10.55.66.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:10 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:796 (796.0 B) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

最後透過 ifconfig 等指令再次觀察,可以看到相關的網卡都起來了,同時 IP 等資訊也都有了,可以準備邁入最後階段

Ping 測試

這個階段,我們要來透過 PING 的指令來打測試我們的連線,整個封包流程會是

c1 容器內的 ping -> c1 容器內的網卡 eth0 -> 宿主機內的 veth0 -> 宿主機內的 Linux Bridge -> 宿主機內 Bridge 上的 veth1 -> c2 容器內的網卡 eth0

這部份我們直接進入到容器內執行 ping 10.55.66.3 指令即可

程式碼模擬

$ docker exec -it c1 ping 10.55.66.3 -c5

這時候你會發現網路不通, ping 都沒有反應,這部份我們執行下面的神祕指令

$ sudo iptables --policy FORWARD ACCEPT
$ docker exec -it c1 ping 10.55.66.3 -c5

你就會發現網路通了,一切都正常了,至於到底上面那個神祕指令做了什麼手腳,為什麼會影響封包,這部份等我們下一章節再來慢慢細談 iptables 的概念

網路模型

最後這邊就用來顯示一下封包的流程,基本上就是概括了本章節提到的所有元件,包含了 veth, Linux Bridge, 容器 IP 等資訊。

Summary

到這邊為止,我們已經瞭解如何將一個什麼都沒有的容器給打造為使用 Bridge 模型的容器,然而現在這種情況下,我們的兩個容器只能彼此互相存取,還沒有辦法對外上網,譬如 ping 8.8.8.8

所以下篇文章我們會來探討這後半部分,來把整個 Docker Bridge 網路模型給摸熟,透過這些步驟來瞭解到底每個容器起來時實際上系統做了什麼手腳

題外話: 上述的流程中有一個部分其實不好處理,稱為 IPAM,也就是 IP 分配管理的相關議題,我們要如何對每個容器去分配一個不重複的 IP 地址,同時當容器掛掉時,也要能夠回收這些 IP。

另外本篇文章的詳細內容其實跟 Bridge CNI 的步驟大概有 80% 一樣, 差異只剩下 IP 分配問題,以及後續會探討到的 IP 路由表管理。