部落格好读版


在上一章中,我们成功让容器之间的封包可以正常传递,但容器对外的封包传递问题仍未解决。本章我们将逐步探讨这个问题,首先来检查 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

    路由表(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)",会发生什么事。

  • 由于 172.18.0.2 不被其他 Destination 条目选中,所以它跳到了条目 1,走网关 172.31.32.1。
  • 来到条目 5,不需要转发 (没有 flag G),因此往外网送了,但外网并没有 172.18.0.2 这个 IP,因此得不到回应,就卡在这里。
  • $ 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 ping 位于 ns0 的 veth0 IP (172.18.0.2) 的连线没有回应。
  • 从 ns1 ping Host IP (172.31.39.53) 的连线无法到达。
  • 解决 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 解释