使用容器总是感觉像使用魔法一样。对于那些理解底层原理的人来说容器很好用,但是对于不理解的人来说就是个噩梦。很幸运的是,我们已经研究容器技术很久了,甚至成功揭秘容器只是隔离并受限的 Linux 进程,运行容器并不需要镜像,以及另一个方面,构建镜像需要运行一些容器。
现在是时候解决容器网络问题了。或者更准确地说,单主机容器网络问题。本文会回答这些问题:
-
如何虚拟化网络资源,让容器认为自己拥有独占网络?
-
如何让容器们和平共处,之间不会互相干扰,并且能够互相通信?
-
从容器内部如何访问外部世界(比如,互联网)?
-
从外部世界如何访问某台机器上的容器呢(比如,端口发布)?
最终结果很明显,单主机容器网络是已知的 Linux 功能的简单组合:
-
网络命名空间(namespace)
-
虚拟 Ethernet设备(veth)
-
虚拟网络交换机(网桥)
-
IP路由和网络地址翻译(NAT)
-
并且不需要任何代码就可以让这样的网络魔法发生……
前提条件
任意 Linux 发行版都可以。本文例子都是在 vagrant CentOS 8 的虚拟机上执行的:
$ vagrant init centos/8
$ vagrant up
$ vagrant ssh
[vagrant@localhost ~]$ uname -a
Linux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64
为了简单起见,本文使用容器化解决方案(比如,Docker 或者 Podman)。我们会重点介绍基本概念,并使用最简单的工具来达到学习目标。
network 命名空间隔离容器
Linux 网络栈包括哪些部分?显然,是一系列网络设备。还有别的吗?可能还包括一系列的路由规则。并且不要忘记,netfilter hook,包括由iptables规则定义的。
我们可以快速创建一个并不复杂的脚本inspect-net-stack.sh
:
#!/usr/bin/env bash
echo "> Network devices"
ip link
echo -e "\n> Route table"
ip route
echo -e "\n> Iptables rules"
iptables --list-rules
在运行脚本前,让我们修改下 iptable rule:
$ sudo iptables -N ROOT_NS
这之后,在机器上执行上面的脚本,输出如下:
$ sudo ./inspect-net-stack.sh
> Network devices
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff
> Route table
default via 10.0.2.2 dev eth0 proto dhcp metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
> Iptables rules
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N ROOT_NS
我们对这些输出感兴趣,因为要确保即将创建的每个容器都有各自独立的网络栈。
你可能已经知道了,用于容器隔离的一个 Linux 命名空间是网络命名空间(network namespace)。从 man ip-netns 可以看到,“网络命名空间是网络栈逻辑上的另一个副本,它有自己的路由,防火墙规则和网络设备。”为了简化起见,这是本文使用的唯一的命名空间。我们并没有创建完全隔离的容器,而是将范围限制在网络栈上。
创建网络命名空间的一种方法是 ip 工具,它是 iproute2 的一部分:
$ sudo ip netns add netns0
$ ip netns
netns0
如何使用刚才创建的命名空间呢?一个很好用的命令 nsenter。进入一个或多个特定的命名空间,然后执行指定的脚本:
$ sudo nsenter --net=/var/run/netns/netns0 bash
# 新建的 bash 进程在 netns0 里
$ sudo ./inspect-net-stack.sh
> Network devices 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
> Route table
> Iptables rules
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
从上面的输出可以清楚地看到 bash 进程运行在 netns0 命名空间,这时看到的是完全不同的网络栈。这里没有路由规则,没有自定义的 iptables chain,只有一个 loopback 的网络设备。
使用虚拟的 Ethernet 设备(veth)将容器连接到主机上
如果我们无法和某个专有的网络栈通信,那么它看上去就没什么用。幸运的是,Linux 提供了好用的工具——虚拟 Ethernet设备。从 man veth 可以看到,“veth 设备是虚拟 Ethernet 设备。他们可以作为网络命名空间之间的通道(tunnel),从而创建连接到另一个命名空间里的物理网络设备的桥梁,但是也可以作为独立的网络设备使用。”
虚拟 Ethernet 设备通常都成对出现。不用担心,先看一下创建的脚本:
$ sudo ip link add veth0 type veth peer name ceth0
用这条简单的命令,我们就可以创建一对互联的虚拟 Ethernet 设备。默认选择了 veth0 和 ceth0 这两个名称。
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff
5: ceth0@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff
6: veth0@ceth0: