在上一章中,我們成功實現了 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 的運作模式還差了一點點,這部分我們留到下一章節再繼續。