在上一章中,我們成功實現了 container namespace (ns0/ns1) 與 root namespace 之間的網路連通。不過,當 container namespace 嘗試連接外部網路時卻遇到了問題。接下來讓我們深入探討這個現象背後的原因,並了解解決方案。
容器網路連接外網的問題分析
為了模擬實際環境,我們將使用 AWS VPC 作為測試環境。在這個環境中,VPC CIDR 扮演互聯網的角色,而 EC2 instance 的 Private IP 則等同於 Public IP。我們將使用兩台 EC2 instance 進行測試,並監控網路封包的流向。
提醒:
network-test-1
:練習操作的 EC2,簡稱為 Host1network-test-2
:模擬外網位置的 EC2,簡稱為 Host2
為 EC2 添加 ICMP 規則
由於之前我們建立的 AWS EC2 並沒有允許 ICMP 相關的規則,在這裡先補上。
- 建立 Security Groups -
sg-icmp
:
- 分別對
network-test-1
、network-test-2
添加sg-icmp
:
使用 tcpdump 來監控封包變化。
- 在視窗的 4 個位置的終端,分別使用以下指令:
1# 1. 在 Host1 - ns1 監控 veth1 的 ICMP 封包2sudo ip netns exec ns1 tcpdump -i veth1 -nn icmp3
4# 2. 在 Host1 - root namespace 監控 docker1 的 ICMP 封包5sudo tcpdump -i docker1 -nn icmp6
7# 3. 在 Host1 - root namespace 監控 enX0 的 ICMP 封包8sudo tcpdump -i enX0 -nn icmp9
10# 4. 在 Host2 - 監控 enX0 的 ICMP 封包11sudo tcpdump -i enX0 -nn icmp
接下來我要分別進行兩個測試。
1. 從 Host1 - ns1 對外網發起 ICMP Echo Request
1sudo ip netns exec ns1 ping -c 1 172.31.45.1742# output3PING 172.31.45.174 (172.31.45.174) 56(84) bytes of data.4
5--- 172.31.45.174 ping statistics ---61 packets transmitted, 0 received, 100% packet loss, time 0ms
我們的請求到外網後,就此石沉大海。
2. 從 Host1 - root host 對外網發起 ICMP Echo Request
1ping -c 1 172.31.45.1742# output3PING 172.31.45.174 (172.31.45.174) 56(84) bytes of data.464 bytes from 172.31.45.174: icmp_seq=1 ttl=127 time=0.403 ms5
6--- 172.31.45.174 ping statistics ---71 packets transmitted, 1 received, 0% packet loss, time 0ms8rtt min/avg/max/mdev = 0.403/0.403/0.403/0.000 ms
這次我們成功的完成了 ICMP 的交握。
為什麼會有這樣的差異呢?
- 第1個測試,發送請求的來源是 Private IP
172.18.0.3
- 第2個測試,發送請求的來源是 Public IP
172.31.39.53
這其實是因為,Private IP 在互聯網上並不是唯一的,有可能所有的服務器(EC2)都有一個 172.18.0.3
。因此,在很多路由設置中,預設 Private IP 是會被丟棄的。
所以想要從 Private IP 對外進行連線,我們需要使用 Public IP 對封包進行偽裝一下,也就是 NAT。
NAT 的運作原理
NAT(Network Address Translation)是一種將 Private IP 位址轉換為 Public IP 位址的網路技術。當內部網路的設備需要存取外部網路時,NAT 會:
- 將來源 IP(私有位址)轉換為 Public IP
- 記錄轉換對應關係
- 接收回應時,根據記錄將封包轉發給正確的內部設備
這使得使用 Private IP 的設備得以安全地存取外部網路資源。
其實安裝 Docker 時,也一併添加了相關的 NAT 設定。下面就讓我們來看看。
分析 Docker 的 NAT 規則
- 使用以下指令,查看 iptables nat 規則:
1sudo iptables -t nat -L --line-numbers2# output3Chain PREROUTING (policy ACCEPT)4num target prot opt source destination51 DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL6
7Chain INPUT (policy ACCEPT)8num target prot opt source destination9
10Chain OUTPUT (policy ACCEPT)11num target prot opt source destination121 DOCKER all -- anywhere !ip-127-0-0-0.us-west-2.compute.internal/8 ADDRTYPE match dst-type LOCAL13
14Chain POSTROUTING (policy ACCEPT)15num target prot opt source destination5 collapsed lines
161 MASQUERADE all -- ip-172-17-0-0.us-west-2.compute.internal/16 anywhere17
18Chain DOCKER (2 references)19num target prot opt source destination201 RETURN all -- anywhere anywhere
- 使用
iptables-save
指令,取得比較明確的規則:
1sudo iptables-save -t nat2# Generated by iptables-save v1.8.8 (nf_tables) on Wed Jan 8 10:34:30 20253*nat4: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 DOCKER10-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER11-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE12-A DOCKER -i docker0 -j RETURN13COMMIT14# Completed on Wed Jan 8 10:34:30 2025
跟 DOCKER 相關的規則段落如下:
1-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER2-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER3-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE4-A DOCKER -i docker0 -j RETURN
分析 iptables-save
規則
1. PREROUTING 鏈
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.2
的 ping 8.8.8.8
)被發送時,NAT 的處理邏輯如下:
步驟 1:請求的初始流量
- 來自容器(IP 為
172.17.0.2
)的封包首先通過主機的OUTPUT
和POSTROUTING
鏈。 - 請求的源地址是
172.17.0.2
,屬於172.17.0.0/16
子網,目標地址是8.8.8.8
。
步驟 2:POSTROUTING
鏈處理
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 IP203.0.113.1
)。 - 轉換後的封包:
- 原始封包:
Terminal window 1SRC=172.17.0.2 DST=8.8.8.8 - NAT 後的封包:
Terminal window 1SRC=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
:
1# Amazon Linux 2023 安裝指令2sudo dnf install -y conntrack-tools
- 使用以下命令,啟動監控條目
8.8.8.8
:
1sudo conntrack -E -p icmp | grep "8.8.8.8"
-E
:啟用事件監控模式,實時顯示新增的連接。-p icmp
:只監控 ICMP 相關的事件。
- 在 Host1 打開另一個 Terminal,使用以下指令建立一個容器,然後執行測試指令:
1docker container run --rm -it alpine /bin/sh2# go into the container3/ # ping -c 1 8.8.8.84PING 8.8.8.8 (8.8.8.8): 56 data bytes564 bytes from 8.8.8.8: seq=0 ttl=116 time=8.404 ms6
7--- 8.8.8.8 ping statistics ---81 packets transmitted, 1 packets received, 0% packet loss9round-trip min/avg/max = 8.404/8.404/8.404 ms
- conntrack 紀錄以下結果:
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=272[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 條目
-
[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 後的地址)。
- 回應封包的來源地址是
-
[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 已收到。icmp
、1 30
、src
和dst
:這些字段與第一條類似,描述了 Echo Request 的來源和目標。type=0 code=0
:ICMP 類型和代碼,表示這是一個 ICMP Echo Reply(Ping 回應)。id=27
:與第一條的id
相同,用於將回應與請求匹配。- 無
[UNREPLIED]
標誌:表示目標主機已回應。
流量路徑與 NAT 行為
從 conntrack 的記錄中,我們可以清楚地看到 NAT 的處理流程:
-
請求發送時:
- Container
172.17.0.2
發送 Ping,目標是8.8.8.8
- NAT 將請求的來源地址改為主機的對外 IP
172.31.39.53
- Container
-
回應接收時:
8.8.8.8
回應,目標地址是172.31.39.53
(經 NAT 改寫後的主機地址)- 主機的 NAT 將回應的目標地址恢復為 container 的地址
172.17.0.2
流程總結
步驟 | 原始 IP 地址 | 經 NAT 後的 IP 地址 |
---|---|---|
請求(容器 → 目標) | src=172.17.0.2 → dst=8.8.8.8 | src=172.31.39.53 → dst=8.8.8.8 |
回應(目標 → 容器) | src=8.8.8.8 → dst=172.31.39.53 | src=8.8.8.8 → dst=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 規則:
1sudo iptables -t nat -I POSTROUTING -s 172.18.0.0/24 ! -o docker1 -j MASQUERADE
- 確認規則內容:
1sudo iptables-save -t nat2# Generated by iptables-save v1.8.8 (nf_tables) on Wed Jan 8 19:54:37 20253*nat4: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 DOCKER10-A PREROUTING -j TRACE11-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER12-A POSTROUTING -s 172.18.0.0/24 ! -o docker1 -j MASQUERADE13-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE14-A POSTROUTING -j TRACE15-A DOCKER -i docker0 -j RETURN2 collapsed lines
16COMMIT17# Completed on Wed Jan 8 19:54:37 2025
- 測試從 ns1 ICMP ECHO Request 到外網:
這次我們成功的連到外網了。
總結
在本章中,我們深入了解了 NAT 的運作原理,並成功實現了讓 container namespace 的網路封包能夠存取外部網路。然而,這個成功是建立在我們先前將 FORWARD 鏈策略設為 ACCEPT 的基礎上。如果將策略恢復為預設的 DROP,container namespace 就會再次失去與外部網路的連線。
- 將 FORWARD 規則調整回
DROP
:
1sudo iptables --policy FORWARD DROP
- 測試從 ns1 ICMP ECHO Request 到外網:
1sudo ip netns exec ns1 ping -c 1 8.8.8.82# output3PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.4
5--- 8.8.8.8 ping statistics ---61 packets transmitted, 0 received, 100% packet loss, time 0ms
看起來我們離 Docker 的運作模式還差了一點點,這部分我們留到下一章節再繼續。