在上一章中,我們成功實現了 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 相關的規則,在這裡先補上。
sg-icmp
:network-test-1
、network-test-2
添加 sg-icmp
:使用 tcpdump 來監控封包變化。
# 1. 在 Host1 - ns1 監控 veth1 的 ICMP 封包
sudo ip netns exec ns1 tcpdump -i veth1 -nn icmp
# 2. 在 Host1 - root namespace 監控 docker1 的 ICMP 封包
sudo tcpdump -i docker1 -nn icmp
# 3. 在 Host1 - root namespace 監控 enX0 的 ICMP 封包
sudo tcpdump -i enX0 -nn icmp
# 4. 在 Host2 - 監控 enX0 的 ICMP 封包
sudo tcpdump -i enX0 -nn icmp
接下來我要分別進行兩個測試。
1. 從 Host1 - ns1 對外網發起 ICMP Echo Request
sudo ip netns exec ns1 ping -c 1 172.31.45.174
# output
PING 172.31.45.174 (172.31.45.174) 56(84) bytes of data.
--- 172.31.45.174 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
我們的請求到外網後,就此石沉大海。
2. 從 Host1 - root host 對外網發起 ICMP Echo Request
ping -c 1 172.31.45.174
# output
PING 172.31.45.174 (172.31.45.174) 56(84) bytes of data.
64 bytes from 172.31.45.174: icmp_seq=1 ttl=127 time=0.403 ms
--- 172.31.45.174 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.403/0.403/0.403/0.000 ms
這次我們成功的完成了 ICMP 的交握。
為什麼會有這樣的差異呢?
172.18.0.3
172.31.39.53
這其實是因為,Private IP 在互聯網上並不是唯一的,有可能所有的服務器(EC2)都有一個 172.18.0.3
。因此,在很多路由設置中,預設 Private IP 是會被丟棄的。
所以想要從 Private IP 對外進行連線,我們需要使用 Public IP 對封包進行偽裝一下,也就是 NAT。
NAT(Network Address Translation)是一種將 Private IP 位址轉換為 Public IP 位址的網路技術。當內部網路的設備需要存取外部網路時,NAT 會:
這使得使用 Private IP 的設備得以安全地存取外部網路資源。
其實安裝 Docker 時,也一併添加了相關的 NAT 設定。下面就讓我們來看看。
sudo iptables -t nat -L --line-numbers
# output
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
1 DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
num target prot opt source destination
Chain OUTPUT (policy ACCEPT)
num target prot opt source destination
1 DOCKER all -- anywhere !ip-127-0-0-0.us-west-2.compute.internal/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
num target prot opt source destination
1 MASQUERADE all -- ip-172-17-0-0.us-west-2.compute.internal/16 anywhere
Chain DOCKER (2 references)
num target prot opt source destination
1 RETURN all -- anywhere anywhere
iptables-save
指令,取得比較明確的規則:sudo iptables-save -t nat
# Generated by iptables-save v1.8.8 (nf_tables) on Wed Jan 8 10:34:30 2025
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER - [0:0]
-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.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Wed Jan 8 10:34:30 2025
跟 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.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
iptables-save
規則1. PREROUTING 鏈
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
這條規則將目標地址類型是 LOCAL
的流量轉發到 DOCKER
chain。
PREROUTING
鏈的作用:處理所有進入的流量,在 routing 決策之前。--dst-type LOCAL
:篩選目標為本地主機的流量(例如針對本地 Docker container 的請求)。2. OUTPUT 鏈
-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 鏈
-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 鏈
-A DOCKER -i docker0 -j RETURN
這條規則指定來自 docker0
接口的流量直接返回,不進行進一步處理。
-i docker0
:只針對來自 Docker 網橋(docker0
接口)的流量。RETURN
:結束該鏈處理,返回到主鏈。進一步簡化規則:
172.17.0.0/16
)轉換為主機的 Public IP,實現容器流量的 NAT。docker0
接口之間的流量進行多餘處理。當 Docker 容器中的一個請求(例如來自 172.17.0.2
的 ping 8.8.8.8
)被發送時,NAT 的處理邏輯如下:
步驟 1:請求的初始流量
172.17.0.2
)的封包首先通過主機的 OUTPUT
和 POSTROUTING
鏈。172.17.0.2
,屬於 172.17.0.0/16
子網,目標地址是 8.8.8.8
。步驟 2:POSTROUTING
鏈處理
-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
)。SRC=172.17.0.2 DST=8.8.8.8
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)。172.17.0.2
,並將封包轉發到正確的容器。conntrack 是一個用來查看、操作和管理 Linux 系統中網路連線追蹤表(Connection Tracking)的工具。網路連線追蹤是 Netfilter 防火牆的一部分,負責追蹤進出系統的網路連線狀態。如果封包有被 NAT 修改過,使用 conntrack
可以清楚的紀錄這個過程。
conntrack
:# Amazon Linux 2023 安裝指令
sudo dnf install -y conntrack-tools
8.8.8.8
:sudo conntrack -E -p icmp | grep "8.8.8.8"
-E
:啟用事件監控模式,實時顯示新增的連接。-p icmp
:只監控 ICMP 相關的事件。
docker container run --rm -it alpine /bin/sh
# go into the container
/ # ping -c 1 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=116 time=8.404 ms
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 8.404/8.404/8.404 ms
[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
[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
[NEW]
[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]
[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]
標誌:表示目標主機已回應。從 conntrack 的記錄中,我們可以清楚地看到 NAT 的處理流程:
請求發送時:
172.17.0.2
發送 Ping,目標是 8.8.8.8
172.31.39.53
回應接收時:
8.8.8.8
回應,目標地址是 172.31.39.53
(經 NAT 改寫後的主機地址)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。這裡我就不詳細說明了。
我們已經知道 Docker 添加了什麼樣的 NAT 規則,現在我們要將它應用到我們自行建立的 container namespace 網段上。
sudo iptables -t nat -I POSTROUTING -s 172.18.0.0/24 ! -o docker1 -j MASQUERADE
sudo iptables-save -t nat
# Generated by iptables-save v1.8.8 (nf_tables) on Wed Jan 8 19:54:37 2025
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A PREROUTING -j TRACE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.18.0.0/24 ! -o docker1 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -j TRACE
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Wed Jan 8 19:54:37 2025
這次我們成功的連到外網了。
在本章中,我們深入了解了 NAT 的運作原理,並成功實現了讓 container namespace 的網路封包能夠存取外部網路。然而,這個成功是建立在我們先前將 FORWARD 鏈策略設為 ACCEPT 的基礎上。如果將策略恢復為預設的 DROP,container namespace 就會再次失去與外部網路的連線。
DROP
:sudo iptables --policy FORWARD DROP
sudo ip netns exec ns1 ping -c 1 8.8.8.8
# output
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
看起來我們離 Docker 的運作模式還差了一點點,這部分我們留到下一章節再繼續。