部落格好读版
在上一章中,我们成功让容器之间的封包可以正常传递,但容器对外的封包传递问题仍未解决。本章我们将逐步探讨这个问题,首先来检查 ns1 到 root namespace 之间可能缺少的部分。
回顾
让我们重新回忆上次的操作和讯息:
# 从 ns1 ping Host IP
$ sudo ip netns exec ns1 ping -c 2 172.31.39.53
###
ping: connect: Network is unreachable
######
# 从 Host ping 位于 ns0 的 veth0 IP
$ ping -c 2 172.18.0.2
###
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
^C
--- 172.18.0.2 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1004ms
ping: connect: Network is unreachable 这个错误表示你的系统无法找到通往目标网路的路径。这意味着:
简单来说,这讯息就是告诉你:「我想去那个地方,但是找不到路。」。
什么是 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,可以使用以下指令安装:
apt-get update
apt-get install net-tools
查询 routing table:
route -n
-n 代表显示 IP 数字,而不要显示 hostnames。
查询结果类似如下:
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 100 0 0 eth0
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
10.0.0.0 192.168.1.254 255.255.255.0 UG 200 0 0 eth0
172.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 路由表的内容:
$ sudo ip netns exec ns1 route -n
###
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
172.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 封包,这限制了它对某些情况的适用性。
$ sudo ip netns exec ns1 tracepath -n 172.18.0.2
1?: [LOCALHOST] pmtu 1500
1: 172.18.0.2 0.063ms reached
1: 172.18.0.2 0.042ms reached
Resume: pmtu 1500 hops 1 back 2
但很显然的,当我们的目标是 Host IP (172.31.39.53) 时,这张表就无法指引我们该往哪里去,因此就失败了。
sudo ip netns exec ns1 tracepath -n 172.31.39.53
1: send failed
Resume: pmtu 65535
查看 root namespace 路由表
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.31.0.2 172.31.32.1 255.255.255.255 UGH 512 0 0 enX0
172.31.32.0 0.0.0.0 255.255.240.0 U 512 0 0 enX0
172.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)",会发生什么事。
$ tracepath -n -m 10 172.18.0.2
1?: [LOCALHOST] pmtu 9001
1: no reply
2: no reply
3: no reply
4: no reply
5: no reply
6: no reply
7: no reply
8: no reply
9: no reply
10: no reply
Too many hops: pmtu 9001
Resume: pmtu 9001
调整路由表
接下来我们要透过调整路由表,来解决刚刚论证的两个问题:
解决 Host 到 ns0 的问题
第一个问题出乎意料的好解决,只需要为 docker1 这个网路介面加上可以覆盖 vth0, vth1 的 IP 网段:
sudo ip addr add 172.18.0.1/24 dev docker1
查询 docker1 的 iptable:
$ ip -br addr
###
lo UNKNOWN 127.0.0.1/8 ::1/128
enX0 UP 172.31.39.53/20 metric 512 fe80::49d:2ff:fe88:529d/64
docker0 DOWN 172.17.0.1/16
docker1 UP 172.18.0.1/24 fe80::f8b6:c0ff:fe55:3371/64
veth0-br@if6 UP fe80::7c56:1aff:fe0a:5c8b/64
veth1-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 时,内核会同步更新路由表:
$ route -n
###
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.18.0.0 0.0.0.0 255.255.255.0 U 0 0 0 docker1 <- 更新的条目
172.31.0.2 172.31.32.1 255.255.255.255 UGH 512 0 0 enX0
172.31.32.0 0.0.0.0 255.255.240.0 U 512 0 0 enX0
172.31.32.1 0.0.0.0 255.255.255.255 UH 512 0 0 enX0
现在我们就可以从 Host 与 veth0 建立连线了:
$ ping -c 2 172.18.0.2
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
64 bytes from 172.18.0.2: icmp_seq=1 ttl=127 time=0.057 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=127 time=0.041 ms
--- 172.18.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1047ms
rtt min/avg/max/mdev = 0.041/0.049/0.057/0.008 ms
$ tracepath -n 172.18.0.2
1?: [LOCALHOST] pmtu 1500
1: 172.18.0.2 0.045ms reached
1: 172.18.0.2 0.009ms reached
Resume: pmtu 1500 hops 1 back 2
解决 ns1 到 Host 的问题
如果我们仔细对比 root namespace 和 ns0, ns1 的路由表,就会发现: ns0, ns1 似乎没有预设路由。
回想一下,我们在 root namespace 中,是有看到预设条目的:
0.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX0
我们在本机的 WSL 环境,开启一个新的 container 来比较一下:
$ docker container run --rm -it alpine /bin/sh
/ # route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
/ # ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.2
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
因此第二个问题的解决方法,就是要为 ns0, ns1 补上缺失的预设条目。指向 docker1 这个网路介面 IP:
# 添加预设条目 - ns0
sudo ip netns exec ns0 ip route add default via 172.18.0.1
# 添加预设条目 - ns1
sudo ip netns exec ns1 ip route add default via 172.18.0.1
查询路由表:
# ns0
$ sudo ip netns exec ns0 ip route
###
default via 172.18.0.1 dev veth0
172.18.0.0/24 dev veth0 proto kernel scope link src 172.18.0.2
######
# ns1
$ sudo ip netns exec ns1 ip route
###
default via 172.18.0.1 dev veth1
172.18.0.0/24 dev veth1 proto kernel scope link src 172.18.0.3
测试结果:
# ns0
$ sudo ip netns exec ns0 ping -c 2 172.31.39.53
###
PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.
64 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.076 ms
64 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.046 ms
--- 172.31.39.53 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.046/0.061/0.076/0.015 ms
######
# ns1
$ sudo ip netns exec ns1 ping -c 2 172.31.39.53
###
PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.
64 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.052 ms
64 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.045 ms
--- 172.31.39.53 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1065ms
rtt min/avg/max/mdev = 0.045/0.048/0.052/0.003 ms
小结
这样一来,我们已经解决了 bridge 之间的封包传递问题,至于容器与外部网路的沟通部分,留到下一章再说。我们下次见。
参考
- Linux 网路设定指南
- Routing Table Entry 解释