使用容器总是感觉很神奇。对于那些了解内部原理和恐怖的人来说,这是一个好方法-对于那些不了解这些信息的人。幸运的是,我们已经在容器化技术的框架下寻找了很长一段时间,甚至设法发现容器只是隔离和受限制的Linux进程,运行容器并不需要真正的映像,并且相反,要生成映像,我们需要运行一些容器。
现在是时候解决容器联网问题了。或更确切地说,是单主机容器网络问题。在本文中,我们将回答以下问题:
如何虚拟化网络资源以使容器认为每个容器都有专用的网络堆栈?
如何将容器变成友好的邻居,防止它们相互干扰,并教好沟通?
如何从外部访问运行在计算机上的容器(又名端口发布)?
结果,很明显,单主机容器网络不过是众所周知的Linux设施的简单组合:
不管是好是坏,不需要任何代码即可实现网络魔术……
任何体面的Linux发行版可能就足够了。本文中的所有示例都是在新的无所事事的CentOS 8虚拟机上制作的:
为了简化示例,在本文中,我们将不依赖任何成熟的容器化解决方案(例如docker或podman)。相反,我们将专注于基本概念,并使用最少的工具来实现我们的学习目标。
什么是Linux网络堆栈?好吧,显然,这是网络设备的集合。还有什么?可能是路由规则集。别忘了,netfilter钩子集(包括iptables规则定义的)。
#!/ usr / bin / env bashecho">网络设备ip linkecho -e&n>路由表" ip routeecho -e" \ n> iptables规则&iptables --list-rules
$ sudo ./inspect-net-stack.sh>网络设备1:lo:< LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue状态未知模式默认组默认qlen 1000链接/环回00:00:00:00:00:00 brd 00:00:00:00:00:002:eth0:< BROADCAST,MULTICAST,UP,LOWER_UP&gt ; mtu 1500 qdisc fq_codel状态UP模式默认组默认qlen 1000链接/以太52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff:ff>通过10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto内核作用域链接src 10.0.2.15 metric 100的路由表默认值> iptables规则-P INPUT ACCEPT -P FORWARD ACCEPT -P OUTPUT ACCEPT -N ROOT_NS
我们对该输出感兴趣,因为我们想确保即将创建的每个容器都将获得一个单独的网络堆栈。
好吧,您可能已经听说过,用于容器隔离的Linux名称空间之一称为网络名称空间。从man ip-netns来看,"网络命名空间在逻辑上是网络堆栈的另一个副本,具有自己的路由,防火墙规则和网络设备。为了简单起见,这是我们将在本文中使用的唯一名称空间。我们没有创建完全隔离的容器,而是将范围限制为仅网络堆栈。
ip工具是创建网络名称空间的一种方法,它是事实上的标准iproute2集合的一部分:
如何开始使用刚创建的名称空间?有一个可爱的Linux命令,称为nsenter。它输入一个或多个指定的名称空间,然后执行给定的程序:
$ sudo nsenter --net = / var / run / netns / netns0 bash#新创建的bash进程位于netns0 $ sudo ./inspect-net-stack.sh>网络设备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>路由表> iptables规则-P输入接受-P转发接受-P输出接受
从上面的输出中可以明显看出,在netns0名称空间内运行的bash进程看到了完全不同的网络堆栈。根本没有路由规则,没有自定义iptables链,只有一个环回网络设备。到现在为止还挺好...
如果我们无法与专用网络堆栈通信,它就不会那么有用。幸运的是,Linux为此提供了合适的工具-虚拟以太网设备!从总体上讲,第九个设备是虚拟以太网设备。它们可以充当网络名称空间之间的隧道,以创建到另一个名称空间中的物理网络设备的桥,但是也可以用作独立的网络设备。
虚拟以太网设备始终成对使用。不用担心,当我们看一下创建命令时,它将很清楚:
使用此命令,我们仅创建了一对互连的虚拟以太网设备。名称veth0和ceth0是任意选择的:
$ ip link1:lo:< LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue状态未知模式默认组默认qlen 1000链接/环回00:00:00:00:00:00 brd 00:00:00:00:00:002:eth0:< BROADCAST,MULTICAST,UP,LOWER_UP&gt ; mtu 1500 qdisc fq_codel状态UP模式默认组默认qlen 1000 link / ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff:ff5:ceth0 @ veth0:< BROADCAST,MULTICAST,M -DOWN> mtu 1500 qdisc noop状态DOWN模式默认组默认qlen 1000 link / ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff:ff6:veth0 @ ceth0:< BROADCAST,MULTICAST,M -DOWN> mtu 1500 qdisc noop状态DOWN模式默认组默认qlen 1000 link / ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff:ff
创建后的veth0和ceth0都位于主机的网络堆栈(也称为根网络名称空间)上。要将根名称空间与netns0名称空间连接,我们需要将其中一台设备保留在根名称空间中,并将另一台设备移至netns0中:
$ sudo ip link set ceth0 netns netns0#列出所有设备,以确保其中之一从根堆栈中消失。$ ip link1:lo:< LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue状态未知模式默认组默认qlen 1000链接/环回00:00:00:00:00:00 brd 00:00:00:00:00:002:eth0:< BROADCAST,MULTICAST,UP,LOWER_UP&gt ; mtu 1500 qdisc fq_codel状态UP模式默认组默认qlen 1000链接/以太52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff6:veth0 @ if5:< BROADCAST,MULTICAST> mtu 1500 qdisc noop状态DOWN模式默认组默认qlen 1000 link / ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff:ff link-netns netns0
一旦打开设备并分配了正确的IP地址,其中一个设备上发生的任何数据包都会立即在连接两个名称空间的对等设备上弹出。让我们从根名称空间开始:
$ sudo nsenter --net = / var / run / netns / netns0 $ ip link set lo up#哎呀$ ip link set ceth0 up $ ip addr add 172.18.0.10/16 dev ceth0 $ ip link1:lo:< LOOPBACK, UP,LOWER_UP> mtu 65536 qdisc noqueue状态未知模式默认组默认qlen 1000链接/回送00:00:00:00:00:00 brd 00:00:00:00:00:005:ceth0 @ if6:< BROADCAST,MULTICAST,UP ,LOWER_UP> mtu 1500 qdisc无队列状态UP模式默认组默认qlen 1000 link / ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff:ff link-netnsid 0
#从`netns0`,ping root veth0 $ ping -c 2 172.18.0.11PING 172.18.0.11(172.18.0.11)56(84)字节数据。来自172.18.0.11的64字节:icmp_seq = 1 ttl = 64个时间= 0.038毫秒从172.18.0.11起的64个字节:icmp_seq = 2 ttl = 64个时间= 0.040毫秒--- 172.18.0.11 ping统计信息--- 2传输数据包,2接收到,0%数据包丢失,时间58msrtt分钟/平均/ max / mdev = 0.038 / 0.039 / 0.040 / 0.001 ms#离开`netns0` $ exit#从根名称空间ping ceth0 $ ping -c 2 172.18.0.10PING 172.18.0.10(172.18.0.10)56(84)字节数据来自172.18.0.10的.64字节:icmp_seq = 1 ttl = 64时间= 0.073毫秒ms来自172.18.0.10的64字节:icmp_seq = 2 ttl = 64时间= 0.046 ms --- 172.18.0.10 ping统计信息--- 2传输的数据包,2收到,0%封包丢失,时间3msrtt最小/平均/最大/ mdev = 0.046 / 0.059 / 0.073 / 0.015 ms
同时,如果我们尝试从netns0命名空间访问任何其他地址,我们将不会成功:
#在根名称空间中$ ip addr show dev eth02:eth0:< BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel状态UP组默认qlen 1000 link / ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255作用域全局动态noprefixroute eth0 valid_lft 84057sec preferred_lft 84057sec inet6 fe80 :: 5054 ; s eth0 $ ping 10.0.2.15connect:网络不可达#尝试从Internet $ ping 8.8.8.8connect:网络不可达
不过,这很容易解释。 netns0路由表中根本没有此类数据包的路由。那里唯一的条目显示了如何到达172.18.0.0/16网络:
Linux有很多填充路由表的方法。其中之一是从直接连接的网络接口提取路由。记住,创建名称空间后,netns0中的路由表为空。但是随后我们在此处添加了ceth0设备,并为其分配了IP地址172.18.0.10/16。由于我们使用的不是简单的IP地址,而是地址和网络掩码的组合,因此网络堆栈设法从中提取路由信息。每个发往172.18.0.0/16网络的数据包都将通过ceth0设备发送。但是其他任何数据包将被丢弃。同样,根名称空间中有一条新路由:
#从`root`名称空间开始:$ ip route#...省略的行... 172.18.0.0/16 dev veth0 proto内核作用域链接src 172.18.0.11
至此,我们准备标记我们的第一个问题。现在我们知道如何隔离,虚拟化和连接Linux网络堆栈。
容器化的整个思想归结为有效的资源共享。即每台机器只有一个容器的情况很少见。相反,目标是在共享环境中运行尽可能多的隔离进程。因此,如果按照上面第7种方法将多个容器放置在同一主机上,会发生什么情况?让我们添加第二个容器:
#从根名称空间$ sudo ip netns添加netns1 $ sudo ip link add veth1类型veth对等名称ceth1 $ sudo ip link set ceth1 netns netns1 $ sudo ip link set veth1 up $ sudo ip addr add 172.18.0.21/16 dev veth1 $ sudo nsenter --net = / var / run / netns / netns1 $ ip链接设置为lo up $ ip链接设置为ceth1 up $ ip addr add 172.18.0.20/16 dev ceth1
#从`netns1`我们无法到达根名称空间!$ ping -c 2 172.18.0.21PING 172.18.0.21(172.18.0.21)56(84)字节数据从172.18.0.20 icmp_seq = 1目标主机UnreachableFrom 172.18.0.20 icmp_seq = 2无法到达目标主机--- 172.18.0.21 ping统计信息--- 2传输的数据包,接收到0个数据包,+ 2个错误,数据包丢失100%,时间55mspipe 2#但是有一条路由!$ ip route172.18.0.0 / 16 dev ceth1原型内核作用域链接src 172.18.0.20#离开`netns1` $ exit#从根名称空间我们无法到达`netns1` $ ping -c 2 172.18.0.20PING 172.18.0.20(172.18.0.20)56(84) )数据字节。从172.18.0.11 icmp_seq = 1目标主机不可达从172.18.0.11 icmp_seq = 2目标主机不可达--- 172.18.0.20 ping统计信息--- 2传输数据包,收到0个错误,+ 2错误,丢失100% ,time 23mspipe 2#从`netns0`我们可以到达`veth1` $ sudo nsenter --net = / var / run / netns / netns0 $ ping -c 2 172.18.0.21PING 172.18.0.21(172.18.0.21)56(84) )数据字节。从172.18.0.2开始为64字节1:icmp_seq = 1 ttl = 64时间= 0.037 ms从172.18.0.21开始的64个字节:icmp_seq = 2 ttl = 64 time = 0.046 ms --- 172.18.0.21 ping统计信息--- 2数据包被发送,2被接收,0%数据包丢失,时间33msrtt min / avg / max / mdev = 0.037 / 0.041 / 0.046 / 0.007 ms#但我们仍然无法达到`netns1` $ ping -c 2 172.18.0.20PING 172.18.0.20(172.18.0.20)56(84)字节从172.18.0.10 icmp_seq = 1目标主机不可达从172.18.0.10 icmp_seq = 2目标主机不可达-172.18.0.20 ping统计信息--- 2数据包传输,0接收,+ 2错误,100%数据包丢失,时间63mspipe 2
哎呀!出了点问题... netns1陷入了困境。由于某种原因,它无法与根对话,并且从根名称空间我们也无法实现。但是,由于两个容器都位于同一个IP网络172.18.0.0/16中,因此我们现在可以从netns0容器与主机veth1进行通信。有趣...
好吧,我花了一些时间才弄清楚,但显然我们正面临路线冲突。让我们检查根名称空间中的路由表:
$ ip route#...省略的行...#172.18.0.0 / 16 dev veth0原型内核作用域链接src 172.18.0.11172.18.0.0 / 16 dev veth1原型内核作用域链接src 172.18.0.21
即使在添加第二对第五根之后,根的网络堆栈也了解到新的路由172.18.0.0/16 dev veth1原型内核作用域链接src 172.18.0.21,但已经存在用于同一网络的路由。当第二个容器尝试ping veth1设备时,正在选择第一个路由以断开连接。如果我们要删除第一个路由sudo ip route delete 172.18.0.0/16 dev veth0 proto内核作用域链接src 172.18.0.11并重新检查连接性,则情况将变成镜像情况。即netns1的连接性将恢复,但netns0将处于困境。
好吧,我相信如果我们为netns1选择另一个IP网络,一切都会正常。但是,一个IP网络中有多个容器是合法的用例。因此,我们需要以某种方式调整第五种方法。
看一下Linux桥-另一个虚拟化网络工具! Linux网桥的行为类似于网络交换机。它在与其连接的接口之间转发数据包。而且由于它是交换机,所以它是在L2(即以太网)级别执行的。
让我们尝试玩我们的新玩具。但是首先,我们需要清理现有的设置,因为到目前为止我们确实不再需要进行某些配置更改。删除网络名称空间就足够了:
$ sudo ip netns删除netns0 $ sudo ip netns删除netns1#但是如果您还有剩余的钱... $ sudo ip link delete veth0 $ sudo ip link delete ceth0 $ sudo ip link delete veth1 $ sudo ip link delete ceth1
快速重新创建两个容器。请注意,我们不会为新的veth0和veth1设备分配任何IP地址:
$ sudo ip netns添加netns0 $ sudo ip链接添加veth0类型veth对等名称ceth0 $ sudo ip链接集veth0 up $ sudo ip链接集ceth0 netns netns0 $ sudo nsenter --net = / var / run / netns / netns0 $ ip链接设置lo up $ ip链接设置ceth0 up $ ip addr添加172.18.0.10/16 dev ceth0 $ exit $ sudo ip netns添加netns1 $ sudo ip link添加veth1类型veth对等名称ceth1 $ sudo ip链接set veth1 up $ sudo ip链接设置ceth1 netns netns1 $ sudo nsenter --net = / var / run / netns / netns1 $ ip链接set lo up $ ip链接set ceth1 up $ ip addr add 172.18.0.20/16 dev ceth1 $ exit
$ ip routedefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto内核作用域链接src 10.0.2.15 metric 100
$ sudo ip链接设置veth0主br0 $ sudo ip链接设置veth1主br0
$ sudo nsenter --net = / var / run / netns / netns0 $ ping -c 2 172.18.0.20PING 172.18.0.20(172.18.0.20)56(84)字节数据。来自172.18.0.20的64字节:icmp_seq = 1 ttl = 64时间= 0.259 ms64字节,来自172.18.0.20:icmp_seq = 2 ttl = 64时间= 0.051 ms-- 172.18.0.20 ping统计信息--- 2数据包传输,2接收,0%数据包丢失,时间2msrtt分钟/平均/最大/ mdev = 0.051 / 0.155 / 0.259 / 0.104毫秒
$ sudo nsenter --net = / var / run / netns / netns1 $ ping -c 2 172.18.0.10 PING 172.18.0.10(172.18.0.10)数据的56(84)字节来自172.18.0.10的64字节:icmp_seq = 1 ttl = 64时间= 0.037 ms64从172.18.0.10开始的64字节:icmp_seq = 2 ttl = 64时间= 0.089 ms-- 172.18.0.10 ping统计信息--- 2传输的数据包,2接收到,0%数据包丢失,时间36msrtt分钟/平均/最大/ mdev = 0.037 / 0.063 / 0.089 / 0.026 ms
可爱!一切正常。使用这种新方法,我们根本不需要配置veth0和veth1。我们分配的唯一两个IP地址分别在ceth0和ceth1末端。但是由于它们都在同一以太网段上(请记住,我们已将它们连接到虚拟交换机),因此在L2级别具有连通性:
$ sudo nsenter --net = / var / run / netns / netns0 $ ip neigh172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de STALE $ exit $ sudo nsenter --net = / var / run / netns / netns1 $ ip neigh172.18.0.10 dev ceth1 lladdr 66:f3:8c:75:09:29 STALE $退出
恭喜,我们学会了如何将容器变成友好的邻居,防止它们相互干扰,但保持连接。
我们的容器可以互相交谈。但是它们可以与主机(即根名称空间)对话吗?
#首先使用exit离开netnet0:$ ping -c 2 172.18.0.10PING 172.18.0.10(172.18.0.10)56(84)字节数据从213.51.1.123 icmp_seq = 1目标网络不可达自213.51.1.123 icmp_seq = 2目标网络不可达--172.18.0.10 ping统计信息--- 2数据包传输,0接收,+ 2错误,100%数据包丢失,时间3ms $ ping -c 2 172.18.0.20PING 172.18.0.20(172.18.0.20) 56(84)字节数据。从213.51.1.123 icmp_seq = 1从目标网络不可达从213.51.1.123 icmp_seq = 2目标网络不可达--- 172.18.0.20 ping统计信息--- 2数据包传输,已接收0,+ 2错误,100丢包百分比,时间3ms
为了在根和容器名称空间之间建立连接,我们需要将IP地址分配给网桥网络接口:
将IP地址分配给网桥接口后,便在主机路由表上获得了一条路由:
$ ip route#...省略的行... 172.18.0.0/16 dev br0 proto内核作用域链接src 172.18.0.1 $ ping -c 2 172.18.0.10PING 172.18.0.10(172.18.0.10)56(84)个字节来自172.18.0.10的data.64字节:icmp_seq = 1 ttl = 64时间= 0.036 ms来自172.18.0.10的data.64字节:icmp_seq = 2 ttl = 64时间= 0.049 ms --- 172.18.0.10 ping统计信息--- 2传输的数据包, 2收到,0%数据包丢失,时间11msrtt min / avg / max / mdev = 0.036 / 0.042 / 0.049 / 0.009 ms $ ping -c 2 172.18.0.20PING 172.18.0.20(172.18.0.20)56(84)字节数据来自172.18.0.20的.64字节:icmp_seq = 1 ttl = 64时间= 0.059毫秒来自172.18.0.20的.64字节:icmp_seq = 2 ttl = 64时间= 0.056 ms --- 172.18.0.20 ping统计信息--- 2传输的数据包,2收到,0%数据包丢失,时间4msrtt最小/平均/最大/ mdev = 0.056 / 0.057 / 0.059 / 0.007 ms
容器也可能具有ping桥接接口的功能,但是它们仍然无法与主机eth0保持联系。我们需要为容器添加默认路由:
$ sudo nsenter --net = / var / run / netns / netns0 $ ip路由通过172.18.0.1添加默认值ping -c 2 10.0.2.15 PING 10.0.2.15(10.0.2.15)56(84)个字节的data.64来自10.0.2.15的字节:icmp_seq = 1 ttl = 64时间= 0.036 ms ms来自10.0.2.15的字节:icmp_seq = 2 ttl = 64时间= 0.053 ms --- 10.0.2.15 ping统计信息--- 2数据包发送,2接收, 0%数据包丢失,时间14msrtt min / avg / max / mdev = 0.036 / 0.044 / 0.053 / 0.010 ms#并重复更改netns1
这项更改基本上将主机变成了路由器,并且网桥接口成为了容器的默认网关。
完美的是,我们将容器与根名称空间相连。现在,让我们尝试将它们连接到外部世界。默认情况下,在Linux中禁用数据包转发(即路由器功能)。我们需要打开它:
好吧,仍然行不通。我们错过了什么?如果容器要将数据包发送到外界,则目标服务器将无法将数据包发送回容器,因为容器的IP地址是私有的。即该特定IP的路由规则仅本地网络知道。世界上许多容器共享完全相同的专用IP地址172.18.0.10。解决此问题的方法称为网络地址转换(NAT)。在进入外部网络之前,由容器产生的数据包将其源IP地址替换为主机的外部接口地址。主持人还将跟踪
......