反過來的問題#
到目前為止解決的都是「裡面想連出去」。但容器跑的多半是 server,會有人從外部要連進來。namespace 裡的 IP(例如 172.17.0.2)是私有位址,外網看不到,需要 host 出面當代理人。
這件事情靠 DNAT(Destination NAT)達成,使用者層的指令是 docker run -p 或 docker-compose 裡的 ports:,叫做 port publishing(埠口公開)或 port mapping(埠口對應)。
三種 IP 角色先分清楚#
外部連線進來時,會經過幾種不同性質的 IP:
- Loopback IP:
127.0.0.0/8,只在本機內部有效,外部看不見 - 私有 IP(private IP):
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,內部網段使用,公網不可路由 - 公開 IP(public IP):可由公網路由的 IP,host 對外那張介面綁的就是這種
容器自己拿的是私有 IP,外部要連進來只能透過 host 在某張介面上監聽的 port,再由 host 把流量轉給容器。
DNAT 規則的位置#
DNAT 必須在路由判斷「這個封包是不是要給我自己」之前發生,所以放在 PREROUTING chain:
sudo iptables -t nat -L PREROUTING -n -v寫一條手動 DNAT 看看,把連到 host eth0 的 8080 port 全部轉給 10.0.0.1:80:
sudo iptables -t nat -A PREROUTING \
-i eth0 -p tcp --dport 8080 \
-j DNAT --to-destination 10.0.0.1:80加上 FORWARD chain 的允許:
sudo iptables -A FORWARD -p tcp -d 10.0.0.1 --dport 80 \
-i eth0 -o br0 -j ACCEPT別忘了上一章已經補過 conntrack 的 RELATED,ESTABLISHED 規則,回程封包靠它放行。
外部連 host_public_ip:8080 時,封包進來的瞬間目的 IP 被改寫成 10.0.0.1:80,再走轉送流程進到 namespace。
Docker 怎麼做這件事#
執行:
docker run -d -p 8080:80 nginx這條指令會發生:
- 在
docker0對應子網(如172.17.0.0/16)配一個 IP 給容器 - 在 iptables 加上一條 PREROUTING DNAT,把進入 host
0.0.0.0:8080的 TCP 流量改寫到172.17.0.X:80 - 在 FORWARD chain 對應的
DOCKERchain 開一條允許
檢查:
sudo iptables -t nat -L DOCKER -n -v
sudo iptables -L DOCKER -n -vDOCKER chain 是 Docker 自動建立的 user-defined chain,PREROUTING 會 jump 進來。所有 -p 開出來的 port 都會在這裡留下對應規則。
三種 publish 寫法的差別#
-p 旗標支援不同寫法:
-p 8080:80:在 host 所有介面(0.0.0.0:8080)publish;外部與本機都連得到-p 127.0.0.1:8080:80:只 bind 在 Loopback,外部完全連不到,只能本機自己連-p 192.168.1.10:8080:80:只 bind 在 host 某張介面的特定 IP
差別反映在 iptables 規則的 -d 欄位上。docker port <container> 可以查實際對應。
檢查 listen 狀態#
sudo ss -tlnp | grep -E ':8080|docker-proxy'可能會看到 docker-proxy 程序在監聽。docker-proxy 是 Docker 用來補足某些 iptables 環境(例如 Loopback 連線)的 user-space 代理;多數情況下 iptables DNAT 才是主路徑。
用 tcpdump 確認封包改寫#
從外部機器連 host_public_ip:8080,在 host 上抓 eth0 與 docker0:
sudo tcpdump -i eth0 -nn 'tcp port 8080'
sudo tcpdump -i docker0 -nn 'tcp port 80'預期看到的樣貌:
eth0上的封包目的是host_public_ip:8080- 同一個連線在
docker0上看到的目的已經是172.17.0.X:80 - 來源 IP 仍是外部那台機器的 IP(DNAT 不改來源)
回程封包透過 conntrack 自動還原,所以外部看到的回應來源仍是 host_public_ip:8080。
安全提醒#
-p 8080:80等同於把容器 expose 到網路上,記得搭配防火牆策略- 內部測試用
-p 127.0.0.1:8080:80限制在 Loopback 比較安全 DOCKER-USERchain 是 Docker 留給使用者額外加規則的位置,會在所有 Docker 自動規則之前評估
全圖串起來#
回頭看完整路徑:
- 容器互通:veth pair + Linux bridge(docker0)
- 容器看得到 host:bridge IP 當 default gateway
- 容器出網:ip_forward + POSTROUTING MASQUERADE
- 外部連回容器:PREROUTING DNAT + port publishing
CNI(Container Network Interface)在 Kubernetes 等環境只是把這套邏輯換成另一組 plugin 來實作,本質上仍然是 namespace、veth、bridge、iptables 這幾塊積木。
延伸閱讀#
man 8 iptables- Docker 文件「Networking overview」
- Kubernetes 文件「Cluster Networking」與 CNI 規範