場景:
- 主機 A(192.168.100.1):
wwwapp(Apache 反向代理,Docker 預設bridge→ 172.17.0.0/16)- 主機 B(192.168.100.2):
webmail(Roundcube,Docker 自建mail-network→ 172.24.0.0/16,-p 83:8000)
目標:把webmail容器搬到 主機 A,讓wwwapp反向代理https://webmail.nuface.tw→http://192.168.100.1:83/。
症狀與線索
- Apache 錯誤:
No route to host/failed to make connection to backend: 192.168.100.1:83- 或
DNS lookup failure for: mailwebmailnuface(同網段 service name 解析不到)
- 容器內工具缺:
curl/nc/ip route不便測試。 - 用
--network=container:wwwapp進測,/etc/resolv.conf指向 8.8.8.8/8.8.4.4(legacy 模式,沒有 127.0.0.11 內建 DNS)。 - 反之在
mail-network的容器,/etc/resolv.conf為 127.0.0.11(Docker 內建 DNS)。 - 在主機 A 上
iptables僅見DOCKER-USER/DOCKER-FORWARD,不像主機 B 那樣有一大串DOCKER-ISOLATION-*、per-bridge 規則。 - 關鍵發現:
filter INPUT沒有允許 Docker 子網進入(預設 DROP);補上「允許 172.17/16 與 172.24/16 進 INPUT」後,全部恢復正常。
問題根因(多重)
- 容器 → 主機 的流量被
INPUTpolicy 擋掉- 髮夾 DNAT 路徑:
wwwapp容器→ 主機 192.168.100.1:83 →(nat/PREROUTING→DOCKER DNAT)→webmail容器:8000 - 封包第一站是 主機 INPUT,若
INPUT預設 DROP 且未放行來自docker0/mail-network,DNAT 根本進不去。
- 髮夾 DNAT 路徑:
- 跨網段 service name 解析的預期誤解
- Docker 只會在「同一個 network」 幫你解析容器名稱(embedded DNS)。
wwwapp若只在bridge(172.17),而webmail在mail-network(172.24),彼此不會互相解析容器名。- 解法:用 主機 IP + 發布埠(例:
http://192.168.100.1:83/),或把兩個容器接到同一個 network。
- iptables 後端不一致(legacy vs nft)可能造成你“看不到” Docker 寫入的規則
- 舊機:
iptables v1.4.21(legacy) - 新機:
iptables v1.8.10 (nf_tables)(nft) - 若 Docker 用 nft、你用 legacy(或反之),會像「規則不見/不完整」。
- 舊機:
- 橋接封包是否進入 iptables 與回包政策
br_netfilter未載入、net.bridge.bridge-nf-call-iptables未開,會讓橋接流量繞過 iptables。- 多橋接/PPPoE 場景,
rp_filter開啟可能導致回包被丟棄(建議關閉或調成寬鬆)。
關鍵概念快速複習
- 髮夾 DNAT(Hairpin NAT):容器打主機的發布埠,再被 DNAT 到同機另一個容器。關鍵點:
- INPUT 要先放行容器打主機的那個埠(例如 83)。
FORWARD必須允許 docker0 ↔ 目標bridge 的東西向流量(多為 Docker 自動處理)。POSTROUTING MASQUERADE讓容器出網/東西向 SNAT 正常回程。
- DOCKER / DOCKER-USER:
DOCKER鏈由 Docker 自己管理,放 DNAT/SNAT、per-bridge 規則。- 自訂請掛在
DOCKER-USER,不要清DOCKER-*鏈。
- 容器 DNS:
- 同網路:內建 127.0.0.11 解析容器名。
- 不同網路:不能互相解析容器名;請用 IP:PORT 或接到同一個網路。
最有用的診斷指令(直接抄)
# 看每個 bridge 的規則是否存在(Docker 自動生成)
iptables -L DOCKER-FORWARD -nv
iptables -L DOCKER-ISOLATION-STAGE-1 -nv
iptables -L DOCKER-ISOLATION-STAGE-2 -nv
iptables -t nat -L DOCKER -nv | sed -n '1,120p'
# 看 POSTROUTING 的 MASQUERADE 對各個 172.x/16 是否存在
iptables -t nat -S | grep MASQUERADE
# 在容器內測 name/IP
docker run --rm --network=container:<app> busybox sh -lc \
'cat /etc/resolv.conf; echo "----"; getent hosts google.com; getent hosts <backend-name>'
# 在 wwwapp 容器內測到主機發布埠(髮夾入口)
docker exec -it wwwapp sh -lc 'wget -qO- --timeout=5 http://192.168.100.1:83/ | head -n 5' || echo FAIL
修復步驟(可擇要)
A) 最小變更:放行「容器 → 主機」的 INPUT
這是這次真正解鎖的一步。
簡易版(你採用的做法):
# 信任 docker 預設網段
iptables -A FW-INPUT -s 172.17.0.0/16 -j ACCEPT
# 信任 mail-network
iptables -A FW-INPUT -s 172.24.0.0/16 -j ACCEPT
更精準(建議其一):
- 用 入介面 放行(比較安全):
iptables -A FW-INPUT -i docker0 -j ACCEPT iptables -A FW-INPUT -i br-64c6b4ce2881 -j ACCEPT # mail-network 的 bridge 名稱 - 或僅放 必要埠(最嚴謹):
# 如果主機需要被容器打 83/TCP(髮夾入口) iptables -A FW-INPUT -i docker0 -p tcp --dport 83 -j ACCEPT # 主機充當 DNS 時 iptables -A FW-INPUT -i docker0 -p udp --dport 53 -j ACCEPT iptables -A FW-INPUT -i docker0 -p tcp --dport 53 -j ACCEPT
B) 讓 Docker 自動規則恢復完整(長期正道)
- 統一 iptables 後端(推薦 nft):
update-alternatives --set iptables /usr/sbin/iptables-nft update-alternatives --set ip6tables /usr/sbin/ip6tables-nft - 橋接封包進 iptables:
modprobe br_netfilter sysctl -w net.bridge.bridge-nf-call-iptables=1 sysctl -w net.bridge.bridge-nf-call-ip6tables=1 - 確認 Docker 控管 iptables:
docker info | egrep -i 'firewall|iptables' # Firewall: true - 重啟 Docker 讓 DOCKER-* 鏈重建:
systemctl restart docker
C) rp_filter 與多橋接/PPPoE
sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.docker0.rp_filter=0
sysctl -w net.ipv4.conf.br-64c6b4ce2881.rp_filter=0
sysctl -w net.ipv4.conf.br.eno1.5.rp_filter=0
反向代理設定要點(兩種做法)
作法 1:主機 IP + 發布埠(不依賴容器名解析)
wwwapp vhost:
ProxyPass / http://192.168.100.1:83/
ProxyPassReverse / http://192.168.100.1:83/
ProxyPreserveHost On
ProxyTimeout 15
配合上面的
INPUT放行與 DOCKER nat 鏈中的dpt:83 -> 172.24.x.y:8000即可。
作法 2:把 wwwapp 接到 mail-network
這樣可用容器名:
ProxyPass / http://mailwebmailnuface:8000/
ProxyPassReverse / http://mailwebmailnuface:8000/
避免了一層髮夾 DNAT,但需要你調整
wwwapp的 network。
最後提供一份「乾淨可重複」的最小規則片段
前提:
INPUT預設 DROP;Docker 用 nft;其餘規則由 Docker 自管。
### INPUT 基礎
iptables -A FW-INPUT -i lo -j ACCEPT
iptables -A FW-INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 允許 docker 桥來的封包進主機(選擇一種寫法)
# 1) 介面層級(建議)
iptables -A FW-INPUT -i docker0 -j ACCEPT
iptables -A FW-INPUT -i br-64c6b4ce2881 -j ACCEPT
# 2) 或僅開放 83/TCP(髮夾入口)
# iptables -A FW-INPUT -i docker0 -p tcp --dport 83 -j ACCEPT
# 其他 INPUT 視需求:SSH/HTTP/HTTPS 等
iptables -A FW-INPUT -p tcp --dport 22 -j ACCEPT
iptables -A FW-INPUT -p tcp --dport 80 -j ACCEPT
iptables -A FW-INPUT -p tcp --dport 443 -j ACCEPT
# 其餘丟棄
iptables -A FW-INPUT -j REJECT --reject-with icmp-host-prohibited
最後總結
- 症狀:反代失敗、DNS 失敗、外網不通,看起來像路由或 Docker 失靈。
- 真因:
filter INPUT沒放行「容器 → 主機」流量,髮夾 DNAT 進不去;另有 nft/legacy 混用與 DNS 誤解。 - 一槍斃命:在 主機 A
INPUT放行來自 docker0 / mail-network bridge 的流量(或至少開 83/TCP)。 - 長期正道:統一到 nft、開
br_netfilter、不要清DOCKER-*鏈,只用DOCKER-USER掛自家規則。 - DNS 心法:不同 Docker network 之間不會互相解析容器名;要嘛接同網路,要嘛用 主機 IP + 發布埠。
這篇就是之後遇到類似「容器互打、反代後端在同機、奇怪連不通」時的完整查表與解法。