Vinc3nt's Life

從 Linux 基礎實現 Docker Bridge 網路:一步步理解容器通訊 (4)

2025-01-10
develop
linux
docker
bridge
container
nat
最後更新:2025-01-26
15分鐘
2960字

在上一章中,我們成功實現了 container namespace (ns0/ns1) 與 root namespace 之間的網路連通。不過,當 container namespace 嘗試連接外部網路時卻遇到了問題。接下來讓我們深入探討這個現象背後的原因,並了解解決方案。

容器網路連接外網的問題分析

為了模擬實際環境,我們將使用 AWS VPC 作為測試環境。在這個環境中,VPC CIDR 扮演互聯網的角色,而 EC2 instance 的 Private IP 則等同於 Public IP。我們將使用兩台 EC2 instance 進行測試,並監控網路封包的流向。

提醒: network-test-1:練習操作的 EC2,簡稱為 Host1 network-test-2:模擬外網位置的 EC2,簡稱為 Host2

為 EC2 添加 ICMP 規則

由於之前我們建立的 AWS EC2 並沒有允許 ICMP 相關的規則,在這裡先補上。

  • 建立 Security Groups - sg-icmp

default

  • 分別對 network-test-1network-test-2 添加 sg-icmp

default

default


使用 tcpdump 來監控封包變化。

  • 在視窗的 4 個位置的終端,分別使用以下指令:

default

Terminal window
1
# 1. 在 Host1 - ns1 監控 veth1 的 ICMP 封包
2
sudo ip netns exec ns1 tcpdump -i veth1 -nn icmp
3
4
# 2. 在 Host1 - root namespace 監控 docker1 的 ICMP 封包
5
sudo tcpdump -i docker1 -nn icmp
6
7
# 3. 在 Host1 - root namespace 監控 enX0 的 ICMP 封包
8
sudo tcpdump -i enX0 -nn icmp
9
10
# 4. 在 Host2 - 監控 enX0 的 ICMP 封包
11
sudo tcpdump -i enX0 -nn icmp

接下來我要分別進行兩個測試。

1. 從 Host1 - ns1 對外網發起 ICMP Echo Request

Terminal window
1
sudo ip netns exec ns1 ping -c 1 172.31.45.174
2
# output
3
PING 172.31.45.174 (172.31.45.174) 56(84) bytes of data.
4
5
--- 172.31.45.174 ping statistics ---
6
1 packets transmitted, 0 received, 100% packet loss, time 0ms

default

我們的請求到外網後,就此石沉大海。

2. 從 Host1 - root host 對外網發起 ICMP Echo Request

Terminal window
1
ping -c 1 172.31.45.174
2
# output
3
PING 172.31.45.174 (172.31.45.174) 56(84) bytes of data.
4
64 bytes from 172.31.45.174: icmp_seq=1 ttl=127 time=0.403 ms
5
6
--- 172.31.45.174 ping statistics ---
7
1 packets transmitted, 1 received, 0% packet loss, time 0ms
8
rtt min/avg/max/mdev = 0.403/0.403/0.403/0.000 ms

default

這次我們成功的完成了 ICMP 的交握。


為什麼會有這樣的差異呢?

  • 第1個測試,發送請求的來源是 Private IP 172.18.0.3
  • 第2個測試,發送請求的來源是 Public IP 172.31.39.53

這其實是因為,Private IP 在互聯網上並不是唯一的,有可能所有的服務器(EC2)都有一個 172.18.0.3。因此,在很多路由設置中,預設 Private IP 是會被丟棄的。

default

所以想要從 Private IP 對外進行連線,我們需要使用 Public IP 對封包進行偽裝一下,也就是 NAT。

NAT 的運作原理

NAT(Network Address Translation)是一種將 Private IP 位址轉換為 Public IP 位址的網路技術。當內部網路的設備需要存取外部網路時,NAT 會:

  1. 將來源 IP(私有位址)轉換為 Public IP
  2. 記錄轉換對應關係
  3. 接收回應時,根據記錄將封包轉發給正確的內部設備

這使得使用 Private IP 的設備得以安全地存取外部網路資源。

default


其實安裝 Docker 時,也一併添加了相關的 NAT 設定。下面就讓我們來看看。

分析 Docker 的 NAT 規則

  • 使用以下指令,查看 iptables nat 規則:
Terminal window
1
sudo iptables -t nat -L --line-numbers
2
# output
3
Chain PREROUTING (policy ACCEPT)
4
num target prot opt source destination
5
1 DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
6
7
Chain INPUT (policy ACCEPT)
8
num target prot opt source destination
9
10
Chain OUTPUT (policy ACCEPT)
11
num target prot opt source destination
12
1 DOCKER all -- anywhere !ip-127-0-0-0.us-west-2.compute.internal/8 ADDRTYPE match dst-type LOCAL
13
14
Chain POSTROUTING (policy ACCEPT)
15
num target prot opt source destination
5 collapsed lines
16
1 MASQUERADE all -- ip-172-17-0-0.us-west-2.compute.internal/16 anywhere
17
18
Chain DOCKER (2 references)
19
num target prot opt source destination
20
1 RETURN all -- anywhere anywhere
  • 使用 iptables-save 指令,取得比較明確的規則:
Terminal window
1
sudo iptables-save -t nat
2
# Generated by iptables-save v1.8.8 (nf_tables) on Wed Jan 8 10:34:30 2025
3
*nat
4
:PREROUTING ACCEPT [0:0]
5
:INPUT ACCEPT [0:0]
6
:OUTPUT ACCEPT [0:0]
7
:POSTROUTING ACCEPT [0:0]
8
:DOCKER - [0:0]
9
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
10
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
11
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
12
-A DOCKER -i docker0 -j RETURN
13
COMMIT
14
# Completed on Wed Jan 8 10:34:30 2025

跟 DOCKER 相關的規則段落如下:

Terminal window
1
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
2
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
3
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
4
-A DOCKER -i docker0 -j RETURN

分析 iptables-save 規則

1. PREROUTING 鏈

Terminal window
1
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER

這條規則將目標地址類型是 LOCAL 的流量轉發到 DOCKER chain。

  • PREROUTING 鏈的作用:處理所有進入的流量,在 routing 決策之前。
  • --dst-type LOCAL:篩選目標為本地主機的流量(例如針對本地 Docker container 的請求)。

2. OUTPUT 鏈

1
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER

這條規則將來自本地主機的非 127.0.0.0/8 流量(例如來自容器的流量)轉發到 DOCKER chain。

  • OUTPUT 鏈的作用:處理由本地生成的流量(例如容器內的流量)。

3. POSTROUTING 鏈

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

這條規則針對源地址為 172.17.0.0/16 的流量,並且輸出接口不是 docker0 的流量執行 MASQUERADE(源地址轉換)。

  • POSTROUTING 鏈的作用:處理在路由決策之後的流量,通常用於 NAT。
  • -s 172.17.0.0/16:針對 Docker 默認網橋(docker0)中的容器生成的流量。
  • ! -o docker0:排除流向 docker0 網橋的流量(即不對容器之間的通信執行 NAT)。
  • MASQUERADE:將流量的源地址轉換為主機的外部地址(例如主機的 Public IP),實現源地址隱藏。

4. DOCKER 鏈

1
-A DOCKER -i docker0 -j RETURN

這條規則指定來自 docker0 接口的流量直接返回,不進行進一步處理。

  • -i docker0:只針對來自 Docker 網橋(docker0 接口)的流量。
  • RETURN:結束該鏈處理,返回到主鏈。

進一步簡化規則:

  • PREROUTING 和 OUTPUT:處理本地或進入流量的目標地址,決定是否需要進一步轉發到 Docker 的內部鏈。
  • POSTROUTING 的 MASQUERADE:將來自 Docker 網橋的源地址(172.17.0.0/16)轉換為主機的 Public IP,實現容器流量的 NAT。
  • DOCKER 鏈的 RETURN:避免對 docker0 接口之間的流量進行多餘處理。

分析從 Docker 容器對外網發出請求,NAT 的處理流程

當 Docker 容器中的一個請求(例如來自 172.17.0.2ping 8.8.8.8)被發送時,NAT 的處理邏輯如下:

步驟 1:請求的初始流量

  • 來自容器(IP 為 172.17.0.2)的封包首先通過主機的 OUTPUTPOSTROUTING 鏈。
  • 請求的源地址是 172.17.0.2,屬於 172.17.0.0/16 子網,目標地址是 8.8.8.8

步驟 2:POSTROUTING 鏈處理

Terminal window
1
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
  • 當請求通過 POSTROUTING 鏈時,Docker 的 NAT 規則檢查到源地址是 172.17.0.2(屬於 172.17.0.0/16),並且請求的出口接口不是 docker0
  • 根據 MASQUERADE 規則,將封包的源地址轉換為主機的外部地址(例如,主機的 Public IP 203.0.113.1)。
  • 轉換後的封包
    • 原始封包:
      Terminal window
      1
      SRC=172.17.0.2 DST=8.8.8.8
    • NAT 後的封包:
      Terminal window
      1
      SRC=203.0.113.1 DST=8.8.8.8

步驟 3:封包進入外網

  • 轉換後的封包被發送到主機的出口接口(例如 eth0),進入外網。
  • 8.8.8.8 接收到來自 203.0.113.1 的請求,而不是來自 172.17.0.2 的請求。

步驟 4:回應處理

  • 8.8.8.8 回應時,回應封包的目標地址是 203.0.113.1(主機的 Public IP)。
  • 回應封包進入主機後,經過 NAT 表的記錄,主機將回應的目標地址轉換回 172.17.0.2,並將封包轉發到正確的容器。

實戰:追蹤 Docker 容器的 NAT 處理流程

conntrack 是一個用來查看、操作和管理 Linux 系統中網路連線追蹤表(Connection Tracking)的工具。網路連線追蹤是 Netfilter 防火牆的一部分,負責追蹤進出系統的網路連線狀態。如果封包有被 NAT 修改過,使用 conntrack 可以清楚的紀錄這個過程。

  • 使用指令安裝 conntrack
Terminal window
1
# Amazon Linux 2023 安裝指令
2
sudo dnf install -y conntrack-tools
  • 使用以下命令,啟動監控條目 8.8.8.8
Terminal window
1
sudo conntrack -E -p icmp | grep "8.8.8.8"

-E:啟用事件監控模式,實時顯示新增的連接。 -p icmp:只監控 ICMP 相關的事件。

  • 在 Host1 打開另一個 Terminal,使用以下指令建立一個容器,然後執行測試指令:
Terminal window
1
docker container run --rm -it alpine /bin/sh
2
# go into the container
3
/ # ping -c 1 8.8.8.8
4
PING 8.8.8.8 (8.8.8.8): 56 data bytes
5
64 bytes from 8.8.8.8: seq=0 ttl=116 time=8.404 ms
6
7
--- 8.8.8.8 ping statistics ---
8
1 packets transmitted, 1 packets received, 0% packet loss
9
round-trip min/avg/max = 8.404/8.404/8.404 ms
  • conntrack 紀錄以下結果:
Terminal window
1
[NEW] icmp 1 30 src=172.17.0.2 dst=8.8.8.8 type=8 code=0 id=27 [UNREPLIED] src=8.8.8.8 dst=172.31.39.53 type=0 code=0 id=27
2
[UPDATE] icmp 1 30 src=172.17.0.2 dst=8.8.8.8 type=8 code=0 id=27 src=8.8.8.8 dst=172.31.39.53 type=0 code=0 id=27

解釋 conntrack 條目

  1. [NEW]

    Terminal window
    1
    [NEW] icmp 1 30 src=172.17.0.2 dst=8.8.8.8 type=8 code=0 id=27 [UNREPLIED] src=8.8.8.8 dst=172.31.39.53 type=0 code=0 id=27
    • [NEW]:這是一個新的連接,表示 ICMP Echo Request 剛剛被發送。
    • icmp:協議是 ICMP。
    • 1 30
      • 1:ICMP 的協議號(固定)。
      • 30:連接的剩餘時間(秒),表示這條連接跟蹤條目會在 30 秒後過期。
    • src=172.17.0.2:來源 IP,表示請求來自容器內(Docker 的虛擬網段)。
    • dst=8.8.8.8:目標 IP,請求發送到 Google 公共 DNS。
    • type=8 code=0:ICMP 類型與代碼,表示這是一個 ICMP Echo Request(Ping 請求)。
    • id=27:ICMP 的識別碼,用於將 Echo Request 和 Echo Reply 匹配。
    • [UNREPLIED]:表示目標(8.8.8.8)尚未回應這個請求。
    • src=8.8.8.8 dst=172.31.39.53
      • 回應封包的來源地址是 8.8.8.8
      • 回應封包的目標地址是 172.31.39.53(主機的內部 IP 地址,經 NAT 後的地址)。
  2. [UPDATE]

    1
    [UPDATE] icmp 1 30 src=172.17.0.2 dst=8.8.8.8 type=8 code=0 id=27 src=8.8.8.8 dst=172.31.39.53 type=0 code=0 id=27
    • [UPDATE]:表示這個連接條目已更新,ICMP Echo Reply 已收到。
    • icmp1 30srcdst:這些字段與第一條類似,描述了 Echo Request 的來源和目標。
    • type=0 code=0:ICMP 類型和代碼,表示這是一個 ICMP Echo Reply(Ping 回應)。
    • id=27:與第一條的 id 相同,用於將回應與請求匹配。
    • [UNREPLIED] 標誌:表示目標主機已回應。

流量路徑與 NAT 行為

從 conntrack 的記錄中,我們可以清楚地看到 NAT 的處理流程:

  1. 請求發送時

    • Container 172.17.0.2 發送 Ping,目標是 8.8.8.8
    • NAT 將請求的來源地址改為主機的對外 IP 172.31.39.53
  2. 回應接收時

    • 8.8.8.8 回應,目標地址是 172.31.39.53(經 NAT 改寫後的主機地址)
    • 主機的 NAT 將回應的目標地址恢復為 container 的地址 172.17.0.2

流程總結

步驟原始 IP 地址經 NAT 後的 IP 地址
請求(容器 → 目標)src=172.17.0.2dst=8.8.8.8src=172.31.39.53dst=8.8.8.8
回應(目標 → 容器)src=8.8.8.8dst=172.31.39.53src=8.8.8.8dst=172.17.0.2

iptables NAT 成功完成了地址轉換,允許容器與外部網絡進行通信。


其實 172.31.39.53 只是我們所在的 AWS EC2 instance 的外網,對於整個互聯網來說,這個 IP 還是內網。不過 AWS 會透過 IGW 做轉換 - 可以將它部分視為 VPC 層級的 NAT,因為它在處理 Public Subnet 流量時,扮演了類似 NAT 的角色,將 Private IP 替換為 Public IP。這裡我就不詳細說明了。

為 container namespace 添加 NAT 規則

我們已經知道 Docker 添加了什麼樣的 NAT 規則,現在我們要將它應用到我們自行建立的 container namespace 網段上。

  • 建立 NAT 規則:
Terminal window
1
sudo iptables -t nat -I POSTROUTING -s 172.18.0.0/24 ! -o docker1 -j MASQUERADE
  • 確認規則內容:
Terminal window
1
sudo iptables-save -t nat
2
# Generated by iptables-save v1.8.8 (nf_tables) on Wed Jan 8 19:54:37 2025
3
*nat
4
:PREROUTING ACCEPT [0:0]
5
:INPUT ACCEPT [0:0]
6
:OUTPUT ACCEPT [0:0]
7
:POSTROUTING ACCEPT [0:0]
8
:DOCKER - [0:0]
9
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
10
-A PREROUTING -j TRACE
11
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
12
-A POSTROUTING -s 172.18.0.0/24 ! -o docker1 -j MASQUERADE
13
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
14
-A POSTROUTING -j TRACE
15
-A DOCKER -i docker0 -j RETURN
2 collapsed lines
16
COMMIT
17
# Completed on Wed Jan 8 19:54:37 2025
  • 測試從 ns1 ICMP ECHO Request 到外網:

default

這次我們成功的連到外網了。

總結

在本章中,我們深入了解了 NAT 的運作原理,並成功實現了讓 container namespace 的網路封包能夠存取外部網路。然而,這個成功是建立在我們先前將 FORWARD 鏈策略設為 ACCEPT 的基礎上。如果將策略恢復為預設的 DROP,container namespace 就會再次失去與外部網路的連線。

  • 將 FORWARD 規則調整回 DROP
Terminal window
1
sudo iptables --policy FORWARD DROP
  • 測試從 ns1 ICMP ECHO Request 到外網:
Terminal window
1
sudo ip netns exec ns1 ping -c 1 8.8.8.8
2
# output
3
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
4
5
--- 8.8.8.8 ping statistics ---
6
1 packets transmitted, 0 received, 100% packet loss, time 0ms

看起來我們離 Docker 的運作模式還差了一點點,這部分我們留到下一章節再繼續。

本文標題:從 Linux 基礎實現 Docker Bridge 網路:一步步理解容器通訊 (4)
文章作者:Vincent Lin
發布時間:2025-01-10