iptables 及 docker 容器網絡分析

本文獨立博客閱讀地址:https://ryan4yin.space/posts/iptables-and-container-networks/

本文僅針對 ipv4 網絡

iptables 提供了包過濾、NAT 以及其他的包處理能力,iptables 應用最多的兩個場景是 firewall 和 NAT

iptables 及新的 nftables 都是基於 netfilter 開發的,是 netfilter 的子項目。

但是 eBPF 社區目前正在開發旨在取代 netfilter 的新項目 bpfilter,他們的目標之一是兼容 iptables/nftables 規則,讓我們拭目以待吧。

概念 - 四表五鏈

實際上還有張 SELinux 相關的 security 表(應該是較新的內核新增的,但是不清楚是哪個版本加的),但是我基本沒接觸過,就略過了。

實際上還有張 SELinux 相關的 security 表(應該是較新的內核新增的,但是不清楚是哪個版本加的),但是我基本沒接觸過,就略過了。

詳細的說明參見 iptables詳解(1):iptables概念 - 朱雙印,這篇文章寫得非常棒!把 iptables 講清楚了。

默認情況下,iptables 提供了四張表(不考慮 security 的話)和五條鏈,數據在這四表五鏈中的處理流程如下圖所示:

在這裏的介紹中,可以先忽略掉圖中 link layer 層的鏈路,它屬於 ebtables 的範疇。另外 conntrack 也暫時忽略,在下一小節會詳細介紹 conntrack 的功能。

對照上圖,對於發送到某個用戶層程序的數據而言,流量順序如下:

  • 首先進入 PREROUTING 鏈,依次經過這三個表: raw -> mangle -> nat
  • 然後進入 INPUT 鏈,這個鏈上也有三個表,處理順序是:mangle -> nat -> filter
  • 過了 INPUT 鏈後,數據纔會進入內核協議棧,最終到達用戶層程序。

用戶層程序發出的報文,則依次經過這幾個表:OUTPUT -> POSTROUTING

從圖中也很容易看出,如果數據 dst ip 不是本機任一接口的 ip,那它通過的幾個鏈依次是:PREROUTEING -> FORWARD -> POSTROUTING

五鏈的功能和名稱完全一致,應該很容易理解。下面按優先級分別介紹下鏈中的四個表:

  • raw: 對收到的數據包在連接跟蹤前進行處理。一般用不到,可以忽略
    • 一旦用戶使用了 RAW 表,RAW 表處理完後,將跳過 NAT 表和 ip_conntrack 處理,即不再做地址轉換和數據包的鏈接跟蹤處理了
  • mangle: 用於修改報文、給報文打標籤
  • nat: 主要用於做網絡地址轉換,SNAT 或者 DNAT
  • filter: 主要用於過濾數據包

數據在按優先級經過四個表的處理時,一旦在某個表中匹配到一條規則 A,下一條處理規則就由規則 A 的 target 參數指定,後續的所有表都會被忽略。target 有如下幾種類型:

  • ACCEPT: 直接允許數據包通過
  • DROP: 直接丟棄數據包,對程序而言就是 100% 丟包
  • REJECT: 丟棄數據包,但是會給程序返回 RESET。這個對程序更友好,但是存在安全隱患,通常不使用。
  • MASQUERADE: (僞裝)將 src ip 改寫爲網卡 ip,和 SNAT 的區別是它會自動讀取網卡 ip。路由設備必備。
  • SNAT/DNAT: 顧名思義,做網絡地址轉換
  • REDIRECT: 在本機做端口映射
  • LOG: 在/var/log/messages文件中記錄日誌信息,然後將數據包傳遞給下一條規則,也就是說除了記錄以外不對數據包做任何其他操作,仍然讓下一條規則去匹配。
    • 只有這個 target 特殊一些,匹配它的數據仍然可以匹配後續規則,不會直接跳過。
  • 其他類型,可以用到的時候再查

理解了上面這張圖,以及四個表的用途,就很容易理解 iptables 的命令了。

常用命令

注意: 下面提供的 iptables 命令做的修改是未持久化的,重啓就會丟失!在下一節會簡單介紹持久化配置的方法。

命令格式:

iptables [-t table] {-A|-C|-D} chain [-m matchname [per-match-options]] -j targetname [per-target-options]

其中 table 默認爲 filter 表,但是感覺系統管理員實際使用最多的是 INPUT 表,用於設置防火牆。

以下簡單介紹在 INPUT 表上添加、修改規則,來設置防火牆:

# --add 允許 80 端口通過
iptables -A INPUT -p tcp --dport 80 -j ACCEPT

# --list-rules 查看所有規則
iptables -S

# --list-rules 查看 INPUT 表中的所有規則
iptables -S INPUT
# 查看 iptables 中的所有規則(比 -L 更詳細)

# ---delete 通過編號刪除規則
iptables -D 1
# 或者通過完整的規則參數來刪除規則
iptables -D INPUT -p tcp --dport 80 -j ACCEPT

# --replace 通過編號來替換規則內容
iptables -R INPUT 1 -s 192.168.0.1 -j DROP

# --insert 在指定的位置插入規則,可類比鏈表的插入
iptables -I INPUT 1 -p tcp --dport 80 -j ACCEPT

# 在匹配條件前面使用感嘆號表示取反
# 如下規則表示接受所有來自 docker0,但是目標接口不是 docker0 的流量
iptables -A FORWARD -i docker0 ! -o docker0 -j ACCEPT

# --policy 設置某個鏈的默認規則
# 很多系統管理員會習慣將連接公網的服務器,默認規則設爲 DROP,提升安全性,避免錯誤地開放了端口。
# 但是也要注意,默認規則設爲 DROP 前,一定要先把允許 ssh 端口的規則加上,否則就尷尬了。
iptables -P INPUT DROP

# --flush 清空 INPUT 表上的所有規則
iptables -F INPUT

本文後續分析時,假設用戶已經清楚 linux bridge、veth 等虛擬網絡接口相關知識。
如果你還缺少這些前置知識,請先閱讀文章 Linux 中的虛擬網絡接口

conntrack 連接跟蹤與 NAT

netfilter 的 conntrack 連接跟蹤功能是 iptables 實現 SNAT/DNAT/MASQUERADE 的前提條件,在上一節給出的數據包處理流程圖中,就有給出 conntrack 生效的位置——在 PREROUTEING 和 OUTPUT 表的 raw 鏈之後生效。

下面以 docker 默認的 bridge 網絡爲例詳細介紹下 conntrack 的功能。

首先,這是我在「Linux 的虛擬網絡接口」文中給出過的 docker0 網絡架構圖:

首先,這是我在「Linux 的虛擬網絡接口」文中給出過的 docker0 網絡架構圖:

+-----------------------------------------------+-----------------------------------+-----------------------------------+
|                      Host                     |           Container A             |           Container B             |
|                                               |                                   |                                   |
|   +---------------------------------------+   |    +-------------------------+    |    +-------------------------+    |
|   |       Network Protocol Stack          |   |    |  Network Protocol Stack |    |    |  Network Protocol Stack |    |
|   +----+-------------+--------------------+   |    +-----------+-------------+    |    +------------+------------+    |
|        ^             ^                        |                ^                  |                 ^                 |
|........|.............|........................|................|..................|.................|.................|
|        v             v  ↓                     |                v                  |                 v                 |
|   +----+----+  +-----+------+                 |          +-----+-------+          |           +-----+-------+         |
|   | .31.101 |  | 172.17.0.1 |      +------+   |          | 172.17.0.2  |          |           |  172.17.0.3 |         |
|   +---------+  +-------------<---->+ veth |   |          +-------------+          |           +-------------+         |
|   |  eth0   |  |   docker0  |      +--+---+   |          | eth0(veth)  |          |           | eth0(veth)  |         |
|   +----+----+  +-----+------+         ^       |          +-----+-------+          |           +-----+-------+         |
|        ^             ^                |       |                ^                  |                 ^                 |
|        |             |                +------------------------+                  |                 |                 |
|        |             v                        |                                   |                 |                 |
|        |          +--+---+                    |                                   |                 |                 |
|        |          | veth |                    |                                   |                 |                 |
|        |          +--+---+                    |                                   |                 |                 |
|        |             ^                        |                                   |                 |                 |
|        |             +------------------------------------------------------------------------------+                 |
|        |                                      |                                   |                                   |
|        |                                      |                                   |                                   |
+-----------------------------------------------+-----------------------------------+-----------------------------------+
         v
    Physical Network  (192.168.31.0/24)

docker 會在 iptables 中爲 docker0 網橋添加如下規則:

-t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

-t filter -P DROP
-t filter -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

這幾行規則使 docker 容器能正常訪問外部網絡。MASQUERADE 在請求出網時,會自動做 SNAT,將 src ip 替換成出口網卡的 ip.
這樣數據包能正常出網,而且對端返回的數據包現在也能正常回到出口網卡。

現在問題就來了:出口網卡收到返回的數據包後,還能否將數據包轉發到數據的初始來源端——某個 docker 容器?難道 docker 還額外添加了與 MASQUERADE 對應的 dst ip 反向轉換規則?

實際上這一步依賴的是本節的主角——iptables 提供的 conntrack 連接跟蹤功能(在「參考」中有一篇文章詳細介紹了此功能)。

連接跟蹤對 NAT 的貢獻是:在做 NAT 轉換時,無需手動添加額外的規則來執行反向轉換以實現數據的雙向傳輸。netfilter/conntrack 系統會記錄 NAT 的連接狀態,NAT 地址的反向轉換是根據這個狀態自動完成的。

比如上圖中的 Container A 通過 bridge 網絡向 baidu.com 發起了 N 個連接,這時數據的處理流程如下:

  • 首先 Container A 發出的數據包被 MASQUERADE 規則處理,將 src ip 替換成 eth0 的 ip,然後發送到物理網絡 192..168.31.0/24
    • conntrack 系統記錄此連接被 NAT 處理前後的狀態信息,並將其狀態設置爲 NEW,表示這是新發起的一個連接
  • 對端 baidu.com 返回數據包後,會首先到達 eth0 網卡
  • conntrack 查表,發現返回數據包的連接已經記錄在表中並且狀態爲 NEW,於是它將連接的狀態修改爲 ESTABLISHED,並且將 dst_ip 改爲 172.17.0.2 然後發送出去
    • 注意,這個和 tcp 的 ESTABLISHED 沒任何關係
  • 經過路由匹配,數據包會進入到 docker0,然後匹配上 iptables 規則:-t filter -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT,數據直接被放行
  • 數據經過 veth 後,最終進入到 Container A 中,交由容器的內核協議棧處理。
  • 數據被 Container A 的內核協議棧發送到「發起連接的應用程序」。

實際測試 conntrack

現在我們來實際測試一下,看看是不是這麼回事:

# 使用 tcpdump 分別在出口網卡 wlp4s0 (相當於 eth0)和 dcoker0 網橋上抓包,後面會用來分析
❯ sudo tcpdump -i wlp4s0 -n > wlp4s0.dump   # 窗口一,抓 wlp4s0 的包
❯ sudo tcpdump -i docker0 -n > docker0.dump  # 窗口二,抓 docker0 的包

現在新建窗口三,啓動一個容器,通過 curl 命令低速下載一個視頻文件:

❯ docker run --rm --name curl -it curlimages/curl "https://media.w3.org/2010/05/sintel/trailer.mp4" -o /tmp/video.mp4 --limit-rate 100k

然後新建窗口四,在宿主機查看 conntrack 狀態

❯ sudo zypper in conntrack-tools  # 這個記得先提前安裝好
❯ sudo conntrack -L | grep 172.17
# curl 通過 NAT 網絡發起了一個 dns 查詢請求,DNS 服務器是網關上的 192.168.31.1
udp      17 22 src=172.17.0.4 dst=192.168.31.1 sport=59423 dport=53 src=192.168.31.1 dst=192.168.31.228 sport=53 dport=59423 [ASSURED] mark=0 use=1
# curl 通過 NAT 網絡向 media.w3.org 發起了 tcp 連接
tcp      6 298 ESTABLISHED src=172.17.0.4 dst=198.18.5.130 sport=54636 dport=443 src=198.18.5.130 dst=192.168.31.228 sport=443 dport=54636 [ASSURED] mark=0 use=1

等 curl 命令跑個十來秒,然後關閉所有窗口及應用程序,接下來進行數據分析:

# 前面查到的,本地發起請求的端口是 54636,下面以此爲過濾條件查詢數據

# 首先查詢 wlp4s0/eth0 進來的數據,可以看到本機的 dst_ip 爲 192.168.31.228.54636
❯ cat wlp4s0.dump | grep 54636 | head -n 15
18:28:28.349321 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [S], seq 750859357, win 64240, options [mss 1460,sackOK,TS val 3365688110 ecr 0,nop,wscale 7], length 0
18:28:28.350757 IP 198.18.5.130.443 > 192.168.31.228.54636: Flags [S.], seq 2381759932, ack 750859358, win 28960, options [mss 1460,sackOK,TS val 22099541 ecr 3365688110,nop,wscale 5], length 0
18:28:28.350814 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [.], ack 1, win 502, options [nop,nop,TS val 3365688111 ecr 22099541], length 0
18:28:28.357345 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [P.], seq 1:518, ack 1, win 502, options [nop,nop,TS val 3365688118 ecr 22099541], length 517
18:28:28.359253 IP 198.18.5.130.443 > 192.168.31.228.54636: Flags [.], ack 518, win 939, options [nop,nop,TS val 22099542 ecr 3365688118], length 0
18:28:28.726544 IP 198.18.5.130.443 > 192.168.31.228.54636: Flags [P.], seq 1:2622, ack 518, win 939, options [nop,nop,TS val 22099579 ecr 3365688118], length 2621
18:28:28.726616 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [.], ack 2622, win 482, options [nop,nop,TS val 3365688487 ecr 22099579], length 0
18:28:28.727652 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [P.], seq 518:598, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 80
18:28:28.727803 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [P.], seq 598:644, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 46
18:28:28.727828 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [P.], seq 644:693, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 49
18:28:28.727850 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [P.], seq 693:728, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 35
18:28:28.727875 IP 192.168.31.228.54636 > 198.18.5.130.443: Flags [P.], seq 728:812, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 84
18:28:28.729241 IP 198.18.5.130.443 > 192.168.31.228.54636: Flags [.], ack 598, win 939, options [nop,nop,TS val 22099579 ecr 3365688488], length 0
18:28:28.729245 IP 198.18.5.130.443 > 192.168.31.228.54636: Flags [.], ack 644, win 939, options [nop,nop,TS val 22099579 ecr 3365688488], length 0
18:28:28.729247 IP 198.18.5.130.443 > 192.168.31.228.54636: Flags [.], ack 693, win 939, options [nop,nop,TS val 22099579 ecr 3365688488], length 0


# 然後再查詢 docker0 上的數據,能發現本地的地址爲 172.17.0.4.54636
❯ cat docker0.dump | grep 54636 | head -n 20
18:28:28.349299 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [S], seq 750859357, win 64240, options [mss 1460,sackOK,TS val 3365688110 ecr 0,nop,wscale 7], length 0
18:28:28.350780 IP 198.18.5.130.443 > 172.17.0.4.54636: Flags [S.], seq 2381759932, ack 750859358, win 28960, options [mss 1460,sackOK,TS val 22099541 ecr 3365688110,nop,wscale 5], length 0
18:28:28.350812 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [.], ack 1, win 502, options [nop,nop,TS val 3365688111 ecr 22099541], length 0
18:28:28.357328 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [P.], seq 1:518, ack 1, win 502, options [nop,nop,TS val 3365688118 ecr 22099541], length 517
18:28:28.359281 IP 198.18.5.130.443 > 172.17.0.4.54636: Flags [.], ack 518, win 939, options [nop,nop,TS val 22099542 ecr 3365688118], length 0
18:28:28.726578 IP 198.18.5.130.443 > 172.17.0.4.54636: Flags [P.], seq 1:2622, ack 518, win 939, options [nop,nop,TS val 22099579 ecr 3365688118], length 2621
18:28:28.726610 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [.], ack 2622, win 482, options [nop,nop,TS val 3365688487 ecr 22099579], length 0
18:28:28.727633 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [P.], seq 518:598, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 80
18:28:28.727798 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [P.], seq 598:644, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 46
18:28:28.727825 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [P.], seq 644:693, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 49
18:28:28.727847 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [P.], seq 693:728, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 35
18:28:28.727871 IP 172.17.0.4.54636 > 198.18.5.130.443: Flags [P.], seq 728:812, ack 2622, win 501, options [nop,nop,TS val 3365688488 ecr 22099579], length 84
18:28:28.729308 IP 198.18.5.130.443 > 172.17.0.4.54636: Flags [.], ack 598, win 939, options [nop,nop,TS val 22099579 ecr 3365688488], length 0
18:28:28.729324 IP 198.18.5.130.443 > 172.17.0.4.54636: Flags [.], ack 644, win 939, options [nop,nop,TS val 22099579 ecr 3365688488], length 0
18:28:28.729328 IP 198.18.5.130.443 > 172.17.0.4.54636: Flags [.], ack 693, win 939, options [nop,nop,TS val 22099579 ecr 3365688488], length 0

能看到數據確實在進入 docker0 網橋前,dst_ip 確實被從 192.168.31.228(wlp4s0 的 ip)被修改爲了 172.17.0.4Container A 的 ip).

NAT 如何分配端口?

上一節我們實際測試發現,docker 容器的流量在經過 iptables 的 MASQUERADE 規則處理後,只有 src ip 被修改了,而 port 仍然是一致的。

但是如果 NAT 不修改連接的端口,實際上是會有問題的。如果有兩個容器同時向 ip: 198.18.5.130, port: 443 發起請求,又恰好使用了同一個 src port,在宿主機上就會出現端口衝突!
因爲這兩個請求被 SNAT 時,如果只修改 src ip,那它們映射到的將是主機上的同一個連接!

這個問題 NAT 是如何解決的呢?我想如果遇到這種情況,NAT 應該會通過一定的規則選用一個不同的端口。

有空可以翻一波源碼看看這個,待續...

如何持久化 iptables 配置

首先需要注意的是,centos7/opensuse 15 都已經切換到了 firewalld 作爲防火牆配置軟件,
而 ubuntu18.04 lts 也換成了 ufw 來配置防火牆。

包括 docker 應該也是在啓動的時候動態添加 iptables 配置。

對於上述新系統,還是建議直接使用 firewalld/ufw 配置防火牆吧,或者網上搜下關閉 ufw/firewalld、啓用 iptables 持久化的解決方案。

本文主要目的在於理解 docker 容器網絡的原理,以及爲後面理解 kubernetes 網絡插件 calico/flannel 打好基礎,因此就不多介紹持久化了。

Docker 如何使用 iptables + 虛擬網絡接口實現容器網絡

通過 docker run 運行容器

首先,使用 docker run 運行幾個容器,檢查下網絡狀況:

# 運行一個 debian 容器和一個 nginx
❯ docker run -dit --name debian --rm debian:buster sleep 1000000
❯ docker run -dit --name nginx --rm nginx:1.19-alpine 

# 查看網絡接口,有兩個 veth 接口(而且都沒設 ip 地址),分別連接到兩個容器的 eth0(dcoker0 網絡架構圖前面給過了,可以往前面翻翻對照下)
❯ ip addr ls
...
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:42:c7:12:ba brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:42ff:fec7:12ba/64 scope link 
       valid_lft forever preferred_lft forever
100: veth16b37ea@if99: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 42:af:34:ae:74:ae brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::40af:34ff:feae:74ae/64 scope link 
       valid_lft forever preferred_lft forever
102: veth4b4dada@if101: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 9e:f1:58:1a:cf:ae brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::9cf1:58ff:fe1a:cfae/64 scope link 
       valid_lft forever preferred_lft forever

# 兩個 veth 接口都連接到了 docker0 上面,說明兩個容器都使用了 docker 默認的 bridge 網絡
❯ sudo brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.024242c712ba       no              veth16b37ea
                                                        veth4b4dada

# 查看路由規則
❯ ip route ls
default via 192.168.31.1 dev wlp4s0 proto dhcp metric 600
#下列路由規則將 `172.17.0.0/16` 網段的所有流量轉發到 docker0
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
192.168.31.0/24 dev wlp4s0 proto kernel scope link src 192.168.31.228 metric 600 

# 查看 iptables 規則
# NAT 表
❯ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
# 所有目的地址在本機的,都先交給 DOCKER 鏈處理一波
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
# (容器訪問外部網絡)所有出口不爲 docker0 的流量,都做下 SNAT,把 src ip 換成出口接口的 ip 地址
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN

# filter 表
❯ sudo iptables -t filter -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
# 所有流量都必須先經過如下兩個表處理,沒問題才能繼續往下走
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -j DOCKER-USER
# (容器訪問外部網絡)出去的流量走了 MASQUERADE,回來的流量會被 conntrack 識別並轉發回來,這裏允許返回的數據包通過。
# 這裏直接 ACCEPT 被 conntrack 識別到的流量
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# 將所有訪問 docker0 的流量都轉給自定義鏈 DOCKER 處理
-A FORWARD -o docker0 -j DOCKER
# 允許所有來自 docker0 的流量通過,不論下一跳是否是 docker0
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
# 下面三個表目前啥規則也沒有,就是簡單的 RETURN,交給後面的表繼續處理
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN

接下來使用如下 docker-compose 配置啓動一個 caddy 容器,添加自定義 network 和端口映射,待會就能驗證 docker 是如何實現這兩種網絡的了。

docker-compose.yml 內容:

version: "3.3"
services:
  caddy:
    image: "caddy:2.2.1-alpine"
    container_name: "caddy"
    restart: always
    command: caddy file-server --browse --root /data/static
    ports:
      - "8081:80"
    volumes:
      - "/home/ryan/Downloads:/data/static"
    networks:
    - caddy-1

networks:
  caddy-1:

現在先用上面的配置啓動 caddy 容器,然後再查看網絡狀況:

# 啓動 caddy
❯ docker-compose up -d
# 查下 caddy 容器的 ip
> docker inspect caddy | grep IPAddress
...
    "IPAddress": "172.18.0.2",

# 查看網絡接口,可以看到多了一個網橋,它就是上一行命令創建的 caddy-1 網絡
# 還多了一個 veth,它連接到了 caddy 容器的 eth0(veth) 接口
❯ ip addr ls
...
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:42:c7:12:ba brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:42ff:fec7:12ba/64 scope link 
       valid_lft forever preferred_lft forever
100: veth16b37ea@if99: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 42:af:34:ae:74:ae brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::40af:34ff:feae:74ae/64 scope link 
       valid_lft forever preferred_lft forever
102: veth4b4dada@if101: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 9e:f1:58:1a:cf:ae brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::9cf1:58ff:fe1a:cfae/64 scope link 
       valid_lft forever preferred_lft forever
103: br-ac3e0514d837: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:7d:95:ba:7e brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-ac3e0514d837
       valid_lft forever preferred_lft forever
    inet6 fe80::42:7dff:fe95:ba7e/64 scope link 
       valid_lft forever preferred_lft forever
105: veth0c25c6f@if104: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-ac3e0514d837 state UP group default 
    link/ether 9a:03:e1:f0:26:ea brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::9803:e1ff:fef0:26ea/64 scope link 
       valid_lft forever preferred_lft forever


# 查看網橋,能看到 caddy 容器的 veth 接口連在了 caddy-1 這個網橋上,沒有加入到 docker0 網絡
❯ sudo brctl show
bridge name     bridge id               STP enabled     interfaces
br-ac3e0514d837         8000.02427d95ba7e       no              veth0c25c6f
docker0         8000.024242c712ba       no              veth16b37ea
                                                        veth4b4dada

# 查看路由,能看到新網橋使用的地址段是 172.18.0.0/16,是 docker0 遞增上來的 
❯ ip route ls
default via 192.168.31.1 dev wlp4s0 proto dhcp metric 600 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
# 多了一個網橋的
172.18.0.0/16 dev br-ac3e0514d837 proto kernel scope link src 172.18.0.1 
192.168.31.0/24 dev wlp4s0 proto kernel scope link src 192.168.31.228 metric 600 

# iptables 中也多了 caddy-1 網橋的 MASQUERADE 規則,以及端口映射的規則
❯ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.18.0.0/16 ! -o br-ac3e0514d837 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
# 端口映射過來的入網流量,都做下 SNAT,把 src ip 換成出口 docker0 的 ip 地址
-A POSTROUTING -s 172.18.0.2/32 -d 172.18.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i br-ac3e0514d837 -j RETURN
-A DOCKER -i docker0 -j RETURN
# 主機上所有其他接口進來的 tcp 流量,只要目標端口是 8081,就轉發到 caddy 容器去(端口映射)
# DOCKER 是被 PREROUTEING 鏈的 target,因此這會導致流量直接走了 FORWARD 鏈,直接繞過了通常設置在 INPUT 鏈的主機防火牆規則!
-A DOCKER ! -i br-ac3e0514d837 -p tcp -m tcp --dport 8081 -j DNAT --to-destination 172.18.0.2:80

❯ sudo iptables -t filter -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
# 給 caddy-1 bridge 網絡添加的轉發規則,與 docker0 的規則完全一一對應,就不多介紹了。
-A FORWARD -o br-ac3e0514d837 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-ac3e0514d837 -j DOCKER
-A FORWARD -i br-ac3e0514d837 ! -o br-ac3e0514d837 -j ACCEPT
-A FORWARD -i br-ac3e0514d837 -o br-ac3e0514d837 -j ACCEPT
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
# 這一條仍然是端口映射相關的規則,接受所有從其他接口過來的,請求 80 端口且出口是 caddy-1 網橋的流量
-A DOCKER -d 172.18.0.2/32 ! -i br-ac3e0514d837 -o br-ac3e0514d837 -p tcp -m tcp --dport 80 -j ACCEPT
# 當存在多個 bridge 網絡的時候,docker 就會在下面兩個 STAGE 鏈中處理將它們隔離開,禁止互相訪問
-A DOCKER-ISOLATION-STAGE-1 -i br-ac3e0514d837 ! -o br-ac3e0514d837 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
# 這裏延續上面 STAGE-1 的處理,徹底隔離兩個網橋的流量
-A DOCKER-ISOLATION-STAGE-2 -o br-ac3e0514d837 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN

到這裏,我們簡單地分析了下 docker 如何通過 iptables 實現 bridge 網絡和端口映射。
有了這個基礎,後面就可以嘗試深入分析 kubernetes 網絡插件 flannel/calico/cilium 了哈哈。

Rootless 容器的網絡實現

如果容器運行時也在 Rootless 模式下運行,那它就沒有權限在宿主機添加 bridge/veth 等虛擬網絡接口,這種情況下,我們前面描述的容器網絡就無法設置了。

那麼 podman/containerd(nerdctl) 目前是如何在 Rootless 模式下構建容器網絡的呢?

查看文檔,發現它們都用到了 rootlesskit 相關的東西,而 rootlesskit 提供了 rootless 網絡的幾個實現,文檔參見 rootlesskit/docs/network.md

其中目前推薦使用,而且 podman/containerd(nerdctl) 都默認使用的方案,是 rootless-containers/slirp4netns

以 containerd(nerdctl) 爲例,按官方文檔安裝好後,隨便啓動幾個容器,然後在宿主機查 iptables/ip addr ls,會發現啥也沒有。
這顯然是因爲 rootless 模式下 containerd 改不了宿主機的 iptables 配置和虛擬網絡接口。但是可以查看到宿主機 slirp4netns 在後臺運行:

❯ ps aux | grep tap
ryan     11644  0.0  0.0   5288  3312 ?        S    00:01   0:02 slirp4netns --mtu 65520 -r 3 --disable-host-loopback --enable-sandbox --enable-seccomp 11625 tap0

但是我看半天文檔,只看到怎麼使用 rootlesskit/slirp4netns 創建新的名字空間,沒看到有介紹如何進入一個已存在的 slirp4netns 名字空間...

使用 nsenter -a -t 11644 也一直報錯,任何程序都是 no such binary...

以後有空再重新研究一波...

總之能確定的是,它通過在虛擬的名字空間中創建了一個 tap 虛擬接口來實現容器網絡,性能相比前面介紹的網絡多少是要差一點的。

nftables

前面介紹了 iptables 以及其在 docker 和防火牆上的應用。但是實際上目前各大 Linux 發行版都已經不建議使用 iptables 了,甚至把 iptables 重命名爲了 iptables-leagacy.

目前 opensuse/debian/opensuse 都已經預裝了並且推薦使用 nftables,而且 firewalld 已經默認使用 nftables 作爲它的後端了

我在 opensuse tumbleweed 上實測,firewalld 添加的是 nftables 配置,而 docker 仍然在用舊的 iptables,也就是說我現在的機器上有兩套 netfilter 工具並存:

# 查看 iptables 數據
> iptables -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o br-e3fbbb7a1b3a -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-e3fbbb7a1b3a -j DOCKER
...

# 確認下是否使用了 nftables 的兼容層,結果提示請我使用 iptables-legacy
> iptables-nft -S
# Warning: iptables-legacy tables present, use iptables-legacy to see them
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT

# 查看 nftables 規則,能看到三張 firewalld 生成的 table
> nft list ruleset
table inet firewalld {
    ...
}
table ip firewalld {
    ...
}
table ip6 firewalld {
    ...
}

但是現在 kubernetes/docker 都還是用的 iptables,nftables 我學了用處不大,以後有空再補充。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章