集装箱联网很简单

2020-10-19 13:50:55

使用容器总是让人感觉很神奇。对于那些了解内部结构的人来说,这是一种好方法,对于那些不了解内部结构的人来说,这是一种可怕的方式。幸运的是,我们已经研究了容器技术很长一段时间,甚至设法发现容器只是孤立的和受限的Linux进程,这些映像并不真的需要运行容器,相反,要构建一个映像,我们需要运行一些容器。

现在是解决集装箱联网问题的时候了。或者,更准确地说,是单主机容器联网问题。在本文中,我们将回答以下问题:

如何虚拟化网络资源,让容器认为每个资源都有一个专用的网络堆栈?

如何把集装箱变成友好的邻居,不让他们干扰,教会他们好好沟通?

如何从外部世界到达运行在机器上的容器(也称为端口发布)?

因此,单主机容器网络只不过是著名的Linux设施的简单组合,这一点将变得显而易见:

不管是好是坏,不需要任何代码就可以实现网络魔术……。

任何像样的Linux发行版可能就足够了。本文中的所有示例都是在全新的流浪CentOS 8虚拟机上实现的:

为简单起见,在本文中,我们不会依赖任何成熟的集装箱化解决方案(例如,docker或podman)。取而代之的是,我们将专注于基本概念,并使用最基本的工具来实现我们的学习目标。

Linux网络堆栈由什么组成?很明显,这套网络设备。还有什么?可能是路由规则集。不要忘记,包括由iptables规则定义的netfilter钩子集。

#!/usr/bin/env bashecho";>;网络设备";IP linkecho-e&34;\n>;路由表";IP routeecho-e";\n>;iptables规则";iptables--list-规则。

$sudo./Inspect-Net-Stack.sh>;网络设备1:lo:<;loopback,up,low_up>;mtu 65536 qdisk无队列状态未知模式默认组默认组默认qlen 1000链路/环回00:00:00:00:00:002:eth0:<;广播,组播,上传,降低_上传>;MTU 1500 qdisk fq_codel state up mode默认组默认qlen 1000 link/ether 52:54:00:e3:27:77 brd ff:ff>;路由表默认通过10.0.2.2 dev eth0 proto dhcp指标10010.0.2.0/24 dev eth0 proto kernel作用域链路src 10.0.2.15指标100>;iptables规则-P输入接受-P转发接受-P输出接受-N ROOT_NS。

我们对该输出很感兴趣,因为我们希望确保即将创建的每个容器都将获得单独的网络堆栈。

您可能已经听说过,用于容器隔离的Linux名称空间之一称为网络名称空间。在MAN IP网络中,网络名称空间在逻辑上是网络堆栈的另一个副本,具有自己的路由、防火墙规则和网络设备。为简单起见,这是我们在本文中要使用的唯一名称空间。与其创建完全隔离的容器,我们宁愿将范围仅限于网络堆栈。

创建网络命名空间的方法之一是IP工具,它是事实上的标准iproute2集合的一部分:

如何开始使用刚刚创建的命名空间?有一个可爱的Linux命令,名为nsenter。它进入一个或多个指定的命名空间,然后执行给定的程序:

$sudo nsenter--net=/var/run/netns/netns0 bash#新创建的bash进程位于netns0$sudo中。/check-net-stack.sh>;网络设备1:LO:<;loopback>;mtu 65536 qdisk noop state down模式默认组默认组默认qlen 1000链路/环回00:00:00:00:00:00 brd 00:00:00:00:00>;路由表>;iptables规则-P输入接受-P转发接受-P输出接受。

从上面的输出可以清楚地看出,在netns0名称空间内运行的bash进程看到了一个完全不同的网络堆栈。根本没有路由规则,没有自定义的iptables链,只有一台环回网络设备。到目前为止,一切都很好..。

如果我们不能与专用网络堆栈通信,那么它就没有多大用处。幸运的是,Linux为此提供了一个合适的工具-虚拟以太网设备!从man Veth来看,Veth设备是虚拟以太网设备。它们可以充当网络名称空间之间的隧道,以创建到另一个名称空间中的物理网络设备的桥,但也可以用作独立的网络设备。

虚拟以太网设备始终成对运行。别担心,当我们看一下创建命令时就会明白了:

使用这个命令,我们刚刚创建了一对互连的虚拟以太网设备。名称veth0和ceth0是任意选择的:

$IP link1:LO:<;LOOPBACK,UP,LOWER_UP>;MTU 65536 qdisk无队列状态未知模式默认组默认组默认qlen 1000 link/loopback 00:00:00:00:00:002:eth0:<;广播,多播,Up,LOWER_UP>;MTU 1500 qdisk fq_codel状态Up模式默认组default qlen 1000 link/ether 52:54:00:e3:27:77 brdff:ff5:ceth0@veth0:<;广播,多播,M-DOWN&>MTU 1500 qdisk noop状态关闭模式默认组默认组默认qlen 1000链路/以太66:2D:24:e3:49:3f BRD ff:ff6:veth0@ceth0:<;广播,多播,M-down>;MTU 1500 qdisk noop状态关闭模式默认组默认组默认qlen 1000链路/以太96:e8:de:1d:22:e0 brd ff:ff。

创建后的veth0和ceth0都驻留在主机的网络堆栈(也称为根网络命名空间)上。要将根命名空间与netns0命名空间连接起来,我们需要将其中一个设备保留在根命名空间中,并将另一个移动到netns0中:

$sudo ip link set ceth0 netns0#列出所有设备,以确保其中一个设备从根堆栈中消失$ip link1:lo:<;loopback,up,low_up>;mtu 65536 qdisk noqueue state未知模式默认组默认qlen 1000 link/loopback 00:00:00:00:00:00:002:eth0:<;Broadcast,Multicast,Up,Low_Up>;MTU 1500 qdisk fq_codel状态向上模式默认组默认qlen 1000链路/以太52:54:00:e3:27:77 brd ff:ff6:veth0@if5:<;广播,多播>;mtu 1500 qdisk noop状态向下模式默认组默认qlen 1000链路/以太96:e8:de:1d:22:e0 brd ff:ff link-netns netns0

一旦我们打开设备并分配正确的IP地址,其中一个设备上出现的任何数据包都会立即在连接两个命名空间的对等设备上弹出。让我们从根命名空间开始:

$sudo nsenter--net=/var/run/netns/netns0$ip link set lo up#whops$ip link set ceth0 up$ip addr add 172.18.0.10/16 dev ceth0$IP link1:LO:<;loopback,up,low_up>;mtu 65536 qdisk noqueue state未知模式默认组默认组默认qlen 1000 link/loopback 00:00:00:00:00:00:00:00:00:00:005:ceth0@if6:<;广播,组播,向上,下_向上和>t;MTU 1500 qdisk noqueue state up模式默认组默认qlen 1000 link/ether 66:2D:24:e3:49:3f brd ff:ff link-netnsid 0。

#from`netns0`,ping root';Sveth0$PING-C2172.18.0.11PING 172.18.0.11(172.18.0.11)56(84)字节数据。来自172.18.0.11的64字节数据:icmp_seq=1TTL=64时间=0.038 ms64字节来自172.18.0.11:icmp_seq=2TTL=64时间=0.040毫秒--172.18.0.11 ping统计数据--发送2个分组,2个接收,0%分组丢失,Time 58msrttmin/avg/max/mdev=0.038/0.039/0.040/0.001 ms#Leave`netns0`$exit#from root命名空间,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 time=0.073 ms64字节from 172.18.0.10:icmp_seq=2 ttl=64 time=0.046 ms--172.18.0.10 ping统计--2个发送的数据包,2个接收的数据包,0%丢包率,时间3msrtt min/avg/max/mdev=0.046/0.059/0.073/0.015 ms。

同时,如果我们试图访问netns0命名空间中的任何其他地址,我们都不会成功:

#inside root命名空间$ip addr show dev eth02:eth0:<;Broadcast,Multicast,Up,Low_Up>;MTU 1500 qdisk fq_codel state up group default qlen 1000 link/ether 52:54:00:e3:27:77 brd ff:ff inet 10.0.2.15/24 brd 10.0.2.255 Scope global dynamic noprefixroute eth0 Valid_lft 84057sec Preference_lft 84057sec inet6 fe80::5054:ff:Fee3:2777/64 Scope link Valid_lft Forever_lft ever#记住这个10.0.2.15$sudo nsenter-net=/var/run/netns/netns0#try host';S eth0$ping 10.0.2.15connect:网络无法访问#尝试从Internet进行操作$ping 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设备发送。但任何其他数据包都将被丢弃。同样,根命名空间中也有一条新路由:

#From`root`命名空间:$IP ROUTE#...。省略行...172.18.0.0/16 dev veth0协议内核作用域链接src 172.18.0.11

在这一点上,我们已经准备好将我们的第一个问题标记为已回答。我们现在知道如何隔离、虚拟化和连接Linux网络堆栈。

集装箱化的整个理念可以归结为高效的资源共享。也就是说,每台机器只有一个容器的情况并不常见。相反,目标是在共享环境中运行尽可能多的独立进程。那么,如果我们按照上面的Veth方法将多个容器放在同一台主机上,会发生什么呢?让我们添加第二个容器:

#from root命名空间$sudo ip netns add netns1$sudo ip link add veth1 type veth对等名称ceth1$sudo IP链路集ceth1 netns1$sudo IP链路集veth1 up$sudo IP addr add 172.18.0.21/16 dev ceth1$sudo nsenter--net=/var/run/netns/netns1$IP link set lo up$IP link set ceth1 up$IP addr add 172.18.0.20/16 dev ceth1。

#from`netns1`我们无法到达根命名空间!$ping-c 2 172.18.0.21PING 172.18.0.21(172.18.0.21)56(84)字节的数据。From 172.18.0.20 icmp_seq=1个目标主机无法到达From 172.18.0.20 icmp_seq=2个目标主机无法到达--172.18.0.21 ping统计信息--2个数据包传输,0个接收,+2个错误,100%数据包丢失,Time 55mspipe 2#但是有一条路由!$ip route172.18.0.0/16 dev ceth1 proto kernel作用域链路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个数据包。+2个错误,100%丢包,时间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.21中的64字节:icmp_seq=1 ttl=64 time=0.037 ms64字节from 172.18.0.21:icmp_seq=2 ttl=64 time=0.046毫秒--172.18.0.21 ping统计信息--2个已发送的信息包,2个已接收,0%信息包丢失,64个信息包丢失,从172.18.0.21开始。21:icmp_seq=2 ttl=64 time=0.046 ms-172.18.0.21 ping统计信息--2个信息包已传输,2个接收,0%信息包丢失,64个信息包丢失,从172.18.0.21开始。Time 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路由号...。省略行...#172.18.0.0/16 dev veth0协议内核作用域链接src 172.18.0.11172.18.0.0/16 dev veth1协议内核作用域链接src 172.18.0.21。

即使在添加第二个veth对之后,root的网络堆栈获知了新的路由172.18.0.0/16dev veth1 proto kernel scope link src 172.18.0.21,但是已经有了完全相同网络的现有路由。当第二个容器尝试ping veth1设备时,选择的第一条路由会中断连接。如果我们删除第一个路由sudo ip route delete 172.18.0.0/16dev veth0 proto kernel scope link src 172.18.0.11并重新检查连接,情况将变成镜像情况。即NetNS1的连通性将被恢复,但是NetNS1将处于不确定状态。

嗯,我相信如果我们为netns1选择另一个IP网络,一切都会正常的。但是,位于一个IP网络中的多个容器是合法的使用案例。因此,我们需要以某种方式调整Veth方法。

看看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 add netns0$sudo ip link add veth0 type veth Peer name ceth0$sudo IP link set veth0 up$sudo IP link set ceth0 netns0$sudo nsenter--net=/var/run/netns/netns0$IP link set lo up$IP addr add 172.18.0.10/16 dev ceth0$exit$sudo IP link add veth1 type veth Peer name ceth1$sudo IP link set veth1 up$sudo IP link set netnsenter-net=/var/run/netns/netns1$IP link up$IP link up$ip add1 addns1 up$sudo IP link set ceth1$sudo IP link set veth1 up$sudo IP link set ceth1$sudo IP link set veth1 up$sudo IP link set ceth1$sudo IP link set veth1 up$sudo IP link set ceth1$sudo IP link set veth1 up$sudo IP link set ceth1 netns1$sudo IP link set veth1 up$sudo IP link set ceth1$ip add1。添加172.18.0.20/16 dev ceth1$exit。

$ip routedefault via 10.0.2.2 dev eth0 proto dhcp指标10010.0.2.0/24 dev eth0协议内核范围链路源10.0.2.15指标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=1ttl=64 time=0.259 ms64字节from 172.18.0.20:icmp_seq=2ttl=64 time=0.051 ms-172.18.0.20 ping统计数据--2个数据包已发送,2个已接收,0%数据包丢失,时间2msrtt min/avg/max/mdev=0.051/0.155/0.259/0.104 ms。

$sudo nsenter--net=/var/run/netns/netns1$ping-c 2 172.18.0.10PING 172.18.0.10(172.18.0.10)56(84)字节数据。来自172.18.0.10的64字节数据:icmp_seq=1ttl=64 time=0.037 ms64字节from 172.18.0.10:icmp_seq=2ttl=64 time=0.089 ms-172.18.0.10 ping统计数据--2个数据包已发送,2个已接收,0%数据包丢失,时间36msrtt min/avg/max/mdev=0.037/0.063/0.089/0.026 ms。

可爱的!。一切都运转得很好。使用这种新方法,我们根本没有配置veth0和veth1。我们仅分配了两个IP地址,分别位于ceth0和ceth1两端。但是,由于它们位于同一以太网段(请记住,我们将它们连接到虚拟交换机),因此在第2层上存在连接:

$sudo nsenter--net=/var/run/netns/netns0$ip he172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de stale$exit$sudo nsenter--net=/var/run/netns/netns1$ip邻居172.18.0.10 dev ceth1 lladdr 66:F3:8c:75:09:29 stale$exit。

祝贺你,我们学会了如何把集装箱变成友好的邻居,防止他们的干扰,但又保持了连通性。

我们的集装箱可以互相通信。但是它们能与主机(即根命名空间)对话吗?

#使用exit先保留`netns0`:$ping-c 2 172.18.0.10PING 172.18.0.10(172.18.0.10)56(84)字节数据。From 213.51.1.123 icmp_seq=1目的网络不可达From 213.51.1.123 icmp_seq=2目的网络不可达--172.18.0.10 ping统计--2个数据包传输,0个接收,+2个错误,100%丢包,时间3ms$ping-c 2 172.18.0.20 ping 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路由号...。省略行...172.18.0.0/16 dev br0协议内核作用域链接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:icmp_seq=1ttl=64 time=0.036 ms64 byte from 172.18.0.10:icmp_seq=2ttl=64 time=0.049 ms-172.18.0.10 ping统计数据-2个数据包已发送,2个已接收,0%数据包丢失,时间11msrttmin/avg/max/mdev=0.036/0.042/0.049/0.009毫秒$PING-C2172.18.0.20PING 172.18.0.20(172.18.0.20)56(84)字节数据来自172.18.0.20的64字节:icmp_seq=1TTL=64time=0.059 ms64字节来自172.18.0.20:icmp_seq=2ttl=64时间=0.056毫秒-172.18.0.20 ping统计-发送2个分组,2个接收,0%分组丢失,时间4msrtt min/avg/max/mdev=0.056/0.057/0.059/0.007 ms。

容器可能还具有ping网桥接口的能力,但它们仍然无法连接到主机的eth0。我们需要添加容器的默认路由:

$sudo nsenter--net=/var/run/netns/netns0$ip route通过172.18.0.1$ping-c 2 10.0.2.15PING 10.0.2.15(10.0.2.15)56(84)字节数据添加默认值。来自10.0.2.15的64字节:icmp_seq=1ttl=64 time=0.036 ms64字节from 10.0.2.15:icmp_seq=2 ttl=64 time=0.053 ms-10.0.2.15 ping统计数据-发送2个数据包,2个接收,0%丢包,Time 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地址替换为主机的外部接口地址。主办方还将跟踪。

.