.

iT邦幫忙

0

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

  • 分享至 

  • xImage
  •  

部落格好讀版


在上一章中,我們成功實現了 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

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


使用 tcpdump 來監控封包變化。

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

# 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 的交握。


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

  • 第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 會:

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

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


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

分析 Docker 的 NAT 規則

  • 使用以下指令,查看 iptables 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:結束該鏈處理,返回到主鏈。

進一步簡化規則:

  • 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 鏈處理

-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
      
    • NAT 後的封包:
      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
# 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 相關的事件。

  • 在 Host1 打開另一個 Terminal,使用以下指令建立一個容器,然後執行測試指令:
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
  • conntrack 紀錄以下結果:
[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

解釋 conntrack 條目

  1. [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 後的地址)。
  2. [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 已收到。
    • 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.8 src=172.31.39.53dst=8.8.8.8
回應(目標 → 容器) src=8.8.8.8dst=172.31.39.53 src=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 規則:
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
  • 測試從 ns1 ICMP ECHO Request 到外網:

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

總結

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

  • 將 FORWARD 規則調整回 DROP
sudo iptables --policy FORWARD DROP
  • 測試從 ns1 ICMP ECHO Request 到外網:
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 的運作模式還差了一點點,這部分我們留到下一章節再繼續。


圖片
  直播研討會

尚未有邦友留言

立即登入留言