在上一章中,我們成功讓容器之間的封包可以正常傳遞,但容器對外的封包傳遞問題仍未解決。本章我們將逐步探討這個問題,首先來檢查 ns1 到 root namespace 之間可能缺少的部分。
回顧
讓我們重新回憶上次的操作和訊息:
1# 從 ns1 ping Host IP2$ sudo ip netns exec ns1 ping -c 2 172.31.39.533###4ping: connect: Network is unreachable5######6# 從 Host ping 位於 ns0 的 veth0 IP7$ ping -c 2 172.18.0.28###9PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.10^C11--- 172.18.0.2 ping statistics ---122 packets transmitted, 0 received, 100% packet loss, time 1004ms
ping: connect: Network is unreachable
這個錯誤表示你的系統無法找到通往目標網路的路徑。這意味著:
- 網路不通:系統無法與目標設備或網路建立連接,可能是因為路由設定錯誤、網路接口未啟動,或者目標設備不可達。
- 路由表 (Routing Table) 問題:系統的路由表可能沒有正確的路由條目來轉發流量到目標網路,或者預設路由設定有問題。
簡單來說,這訊息就是告訴你:「我想去那個地方,但是找不到路。」。
什麼是 Routing Table
路由表(Routing Table)是一張指導網路流量(數據包)如何從源地址送到目標地址的地圖。它告訴系統每一個目標網段應該經由哪條路徑、哪個網關(下一跳)或哪個網路接口來到達目的地。
我認為可以用早期的電話中心(Call Center)來理解路由表。每次打電話需要人工接線員將你的線路插到對方的端口,讓兩人通話。路由表就像這個接線員的「電話分配表」,決定每條線路(數據包)應該接到哪個插孔(下一跳)。
- 路由表 就是接線員的指導工具,幫助接線員快速找到正確的插孔或轉發路徑。
- 查表決定路徑:接線員(系統)依據路由表,找到數據包(電話)應該送到的下一個接口(插孔)。
- 直接連接更快:如果兩個電話在同一交換機(網段內),接線員能直接連接,不需要轉發,效率最高。
- 處理默認情況:當找不到具體的匹配(例如國際長途),接線員將按默認規則轉接到大總機(默認路由)。
路由表的類型
依照前面提到的執行規則,路由表的類型通常可分為以下兩類:
- 內網通信 (Directly connected route):如果目標在同一網段(例如 192.168.1.0/24),數據包會直接發送到目標,不需要經過網關。
- 外網通信 (Remote routes):如果目標地址不在內網範圍,數據包會發送到默認網關,由它負責轉發到外部網路。
Routing Table 的條目組成
每一條路由條目通常會包括:
- Destination:目標網路或 IP 範圍,例如
192.168.1.0/24
(內網)或0.0.0.0/0
(所有未知目標)。 - Gateway:將封包轉發到下一跳的路由器或設備的 IP。例如默認路由(
0.0.0.0
)通常經由網關192.168.1.1
。 - Genmask:確定目標網段的大小,例如:
- 255.255.255.0(/24)
- 255.255.0.0(/16)
- Flags:
U
:表示這條路由是「啟用」的。G
:表示這條路由是指向 gateway(即需要轉發的路由)。
- Metric:路由的優先順序,數字越小優先級越高。
- Iface:使用哪個網路介面(例如 eth0 或 wlan0)來發送封包。
如何查詢 Routing Table
在 Linux 中,可以使用 route
或 ip route
命令來查看路由表。這些命令會顯示系統如何轉發網路封包,尤其是選擇哪些路由來到達不同的目標網路。
我覺得 route
指令比較好理解,不過 Ubuntu 並沒有預先安裝 route
,可以使用以下指令安裝:
1apt-get update2apt-get install net-tools
查詢 routing table:
1route -n
-n
代表顯示 IP 數字,而不要顯示 hostnames。
查詢結果類似如下:
1Destination Gateway Genmask Flags Metric Ref Use Iface20.0.0.0 192.168.1.1 0.0.0.0 UG 100 0 0 eth03192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0410.0.0.0 192.168.1.254 255.255.255.0 UG 200 0 0 eth05172.16.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
路由表的解讀邏輯
- 如果你要送信到
192.168.1.23
(同一個社區),路由表會說:「不需要經過網關,直接送到社區內的地址,使用eth0
」。 - 如果你要送信到
10.0.0.45
(另一個城市),路由表會說:「找到中轉站192.168.1.254
,經過它送信,使用eth0
」。 - 如果地址是其他地方(例如
8.8.8.8
),路由表會說:「不知道怎麼處理,發給默認網關192.168.1.1
來處理,使用eth0
」。
觀察 ns1 路由表
我們先來觀察 ns1 路由表的內容:
1$ sudo ip netns exec ns1 route -n2###3Kernel IP routing table4Destination Gateway Genmask Flags Metric Ref Use Iface5172.18.0.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1
依照上面我們看到的邏輯,當我們 ping 另一個容器 (172.18.0.2
) 時,封包會依照這個條目,透過 veth1 送出去,經由 veth pair 到另一端網路介面 veth1-br,再透過 bridge 傳送到 veth0-br,然後又透過 veth pair 送到另一端的 veth0。這也是為什麼容器與容器間的傳送是暢通的原因。
我們可以使用 tracepath 來追蹤網路封包的路徑(預設使用 UDP 封包),它簡單並且不需要額外的權限,適合快速排查。不過需要注意的是,tracepath 無法追蹤 ICMP 封包,這限制了它對某些情況的適用性。
1$ sudo ip netns exec ns1 tracepath -n 172.18.0.22 1?: [LOCALHOST] pmtu 15003 1: 172.18.0.2 0.063ms reached4 1: 172.18.0.2 0.042ms reached5 Resume: pmtu 1500 hops 1 back 2
但很顯然的,當我們的目標是 Host IP (172.31.39.53) 時,這張表就無法指引我們該往哪裡去,因此就失敗了。
1sudo ip netns exec ns1 tracepath -n 172.31.39.532 1: send failed3 Resume: pmtu 65535
查看 root namespace 路由表
1$ route -n2Kernel IP routing table3Destination Gateway Genmask Flags Metric Ref Use Iface40.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX05172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker06172.31.0.2 172.31.32.1 255.255.255.255 UGH 512 0 0 enX07172.31.32.0 0.0.0.0 255.255.240.0 U 512 0 0 enX08172.31.32.1 0.0.0.0 255.255.255.255 UH 512 0 0 enX0
我們來分析開頭提到的案例,“從 Host ping 位於 ns0 的 veth0 IP (172.18.0.2)“,會發生什麼事。
- 由於
172.18.0.2
不被其他 Destination 條目選中,所以它跳到了條目 1,走網關172.31.32.1
。 - 來到條目 5,不需要轉發 (沒有 flag G),因此往外網送了,但外網並沒有 172.18.0.2 這個 IP,因此得不到回應,就卡在這裡。
1$ tracepath -n -m 10 172.18.0.22 1?: [LOCALHOST] pmtu 90013 1: no reply4 2: no reply5 3: no reply6 4: no reply7 5: no reply8 6: no reply9 7: no reply10 8: no reply11 9: no reply1210: no reply13 Too many hops: pmtu 900114 Resume: pmtu 9001
調整路由表
接下來我們要透過調整路由表,來解決剛剛論證的兩個問題:
- 從 Host ping 位於 ns0 的 veth0 IP (172.18.0.2) 的連線沒有回應。
- 從 ns1 ping Host IP (172.31.39.53) 的連線無法到達。
解決 Host 到 ns0 的問題
第一個問題出乎意料的好解決,只需要為 docker1 這個網路介面加上可以覆蓋 vth0, vth1 的 IP 網段:
1sudo ip addr add 172.18.0.1/24 dev docker1
查詢 docker1 的 iptable:
1$ ip -br addr2###3lo UNKNOWN 127.0.0.1/8 ::1/1284enX0 UP 172.31.39.53/20 metric 512 fe80::49d:2ff:fe88:529d/645docker0 DOWN 172.17.0.1/166docker1 UP 172.18.0.1/24 fe80::f8b6:c0ff:fe55:3371/647veth0-br@if6 UP fe80::7c56:1aff:fe0a:5c8b/648veth1-br@if8 UP fe80::3065:8eff:fe22:180/64
還記得上面提到的 Directly connected route 嗎?
當某個網路介面(interface)有一個 IP 地址時,該網路介面就可以直接訪問與該 IP 屬於同一網段的其他設備。
自動添加的直連路由條目只會在以下情況發生:
- IP 地址包含有子網資訊(例如 /24 的子網掩碼)。
- 介面是啟動狀態(
ip link set <interface> up
)。
Linux 的內核網路堆疊實現了這個自動行為:當你使用 ip addr add
添加 IP 時,內核會同步更新路由表:
1$ route -n2###3Kernel IP routing table4Destination Gateway Genmask Flags Metric Ref Use Iface50.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX06172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker07172.18.0.0 0.0.0.0 255.255.255.0 U 0 0 0 docker1 <- 更新的條目8172.31.0.2 172.31.32.1 255.255.255.255 UGH 512 0 0 enX09172.31.32.0 0.0.0.0 255.255.240.0 U 512 0 0 enX010172.31.32.1 0.0.0.0 255.255.255.255 UH 512 0 0 enX0
現在我們就可以從 Host 與 veth0 建立連線了:
1$ ping -c 2 172.18.0.22PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.364 bytes from 172.18.0.2: icmp_seq=1 ttl=127 time=0.057 ms464 bytes from 172.18.0.2: icmp_seq=2 ttl=127 time=0.041 ms5
6--- 172.18.0.2 ping statistics ---72 packets transmitted, 2 received, 0% packet loss, time 1047ms8rtt min/avg/max/mdev = 0.041/0.049/0.057/0.008 ms
1$ tracepath -n 172.18.0.22 1?: [LOCALHOST] pmtu 15003 1: 172.18.0.2 0.045ms reached4 1: 172.18.0.2 0.009ms reached5 Resume: pmtu 1500 hops 1 back 2
解決 ns1 到 Host 的問題
如果我們仔細對比 root namespace 和 ns0, ns1 的路由表,就會發現: ns0, ns1 似乎沒有預設路由。
回想一下,我們在 root namespace 中,是有看到預設條目的:
10.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX0
我們在本機的 WSL 環境,開啟一個新的 container 來比較一下:
1$ docker container run --rm -it alpine /bin/sh2/ # route -n3Kernel IP routing table4Destination Gateway Genmask Flags Metric Ref Use Iface50.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth06172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth07/ # ip route8default via 172.17.0.1 dev eth09172.17.0.0/16 dev eth0 scope link src 172.17.0.210/ # ip addr111: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 100012 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:0013 inet 127.0.0.1/8 scope host lo14 valid_lft forever preferred_lft forever15 inet6 ::1/128 scope host5 collapsed lines
16 valid_lft forever preferred_lft forever1711: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP18 link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff19 inet 172.17.0.2/16 brd 172.17.255.255 scope global eth020 valid_lft forever preferred_lft forever
因此第二個問題的解決方法,就是要為 ns0, ns1 補上缺失的預設條目。指向 docker1 這個網路介面 IP:
1# 添加預設條目 - ns02sudo ip netns exec ns0 ip route add default via 172.18.0.13# 添加預設條目 - ns14sudo ip netns exec ns1 ip route add default via 172.18.0.1
查詢路由表:
1# ns02$ sudo ip netns exec ns0 ip route3###4default via 172.18.0.1 dev veth05172.18.0.0/24 dev veth0 proto kernel scope link src 172.18.0.26######7# ns18$ sudo ip netns exec ns1 ip route9###10default via 172.18.0.1 dev veth111172.18.0.0/24 dev veth1 proto kernel scope link src 172.18.0.3
測試結果:
1# ns02$ sudo ip netns exec ns0 ping -c 2 172.31.39.533###4PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.564 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.076 ms664 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.046 ms7
8--- 172.31.39.53 ping statistics ---92 packets transmitted, 2 received, 0% packet loss, time 1001ms10rtt min/avg/max/mdev = 0.046/0.061/0.076/0.015 ms11######12# ns113$ sudo ip netns exec ns1 ping -c 2 172.31.39.5314###15PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.6 collapsed lines
1664 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.052 ms1764 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.045 ms18
19--- 172.31.39.53 ping statistics ---202 packets transmitted, 2 received, 0% packet loss, time 1065ms21rtt min/avg/max/mdev = 0.045/0.048/0.052/0.003 ms
小結
這樣一來,我們已經解決了 bridge 之間的封包傳遞問題,至於容器與外部網路的溝通部分,留到下一章再說。我們下次見。