写点什么

手把手带你搞定 4 大容器网络问题

2021 年 2 月 26 日

手把手带你搞定4大容器网络问题

本文最初发布于 Ivan Velichko 的个人博客,经原作者授权由 InfoQ 中文站翻译并分享。


使用容器总是感觉像变魔术一样。对那些了解其内部原理的人来说,它是一种很好的方式;而对那些不了解其内部原理的人来说,这是一种可怕的方式。


幸运的是,我们研究容器化技术的内部原理已经很长一段时间了。我们甚至发现,容器只是隔离的、受限制的Linux进程镜像并不是运行容器所必须的,相反——要构建一个镜像,我们需要运行一些容器


现在,让我们来解决下容器网络问题。或者,更准确地说,是单主机容器网络问题。在本文中,我们将回答以下问题:


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

  • 如何将容器变成友好的邻居,防止它们相互干扰,并教它们如何很好地沟通?

  • 怎样从容器内部访问外部世界(比如互联网)?

  • 如何从外部世界(即端口发布)访问运行在一台机器上的容器?


单主机容器网络只不过是一些众所周知的 Linux 工具的简单组合:


  • 网络命名空间

  • 虚拟以太网设备(veth)

  • 虚拟网络交换机(网桥)

  • IP 路由和网络地址转换(NAT)


不管怎样,不需要任何代码就可以让网络魔法发生……

前提条件

任何还算不错的 Linux 发行版可能都足矣。本文中的所有例子都是在一个全新的 vagrant CentOS 8 虚拟机上完成的:


$ vagrant init centos/8$ vagrant up$ vagrant ssh
[vagrant@localhost ~]$ uname -aLinux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64
复制代码


简单起见,在本文中,我们不打算依赖任何成熟的容器化解决方案(例如 docker 或 podman)。相反,我们将关注基本概念,并使用最简单的工具来实现我们的学习目标。

通过网络命名空间隔离容器

Linux 网络堆栈是由什么组成的?很明显,是网络设备的集合。还有什么?可能是路由规则集。不要忘了还有 netfilter 钩子集,包括由 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
复制代码


在运行它之前,让我们稍微修改下 iptables 规则,让其更容易识别:


$ sudo iptables -N ROOT_NS
复制代码


之后,在我的机器上执行 inspect 脚本会产生以下输出:


$ sudo ./inspect-net-stack.sh> Network devices1: 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:002: 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 tabledefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.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 netnsnetns0
复制代码


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


$ sudo nsenter --net=/var/run/netns/netns0 bash# The newly created bash process lives in netns0
$ sudo ./inspect-net-stack.sh> Network devices1: 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
复制代码


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



使用虚拟以太网设备(veth)将容器连接到主机

如果我们不能与一个专用的网络堆栈通信,那么它就没那么有用了。幸运的是,Linux 为此提供了一个合适工具——虚拟以太网设备!按照man veth的说法,“veth 设备是虚拟以太网设备。它们可以作为网络命名空间之间的隧道,创建一个连接到另一个命名空间中物理网络设备的桥,但也可以作为独立的网络设备使用。”


虚拟以太网设备总是成对出现。不用担心,让我们看一下创建命令就会明白了:


$ sudo ip link add veth0 type veth peer name ceth0
复制代码


通过这个命令,我们刚刚创建了一对相互连接的虚拟以太网设备。名称veth0ceth0是任意的:


$ ip link1: 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:002: 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:ff5: 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:ff6: veth0@ceth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff
复制代码


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


$ sudo ip link set ceth0 netns netns0# List all the devices to make sure one of them disappeared from the root stack$ ip link1: 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:002: 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:ff6: veth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff link-netns netns0
复制代码


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


$ sudo ip link set veth0 up$ sudo ip addr add 172.18.0.11/16 dev veth0
复制代码


接下来是etns0


$ sudo nsenter --net=/var/run/netns/netns0$ ip link set lo up  # whoops$ 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 state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:005: ceth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000    link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff link-netnsid 0
复制代码



通过 veth 设备连接网络命名空间


现在可以检查下连接了:


# From `netns0`, ping root's veth0$ ping -c 2 172.18.0.11PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.038 ms64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms
--- 172.18.0.11 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 58msrtt min/avg/max/mdev = 0.038/0.039/0.040/0.001 ms
# Leave `netns0`$ exit
# From root namespace, ping ceth0$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.073 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.046 ms
--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 3msrtt min/avg/max/mdev = 0.046/0.059/0.073/0.015 ms
复制代码


同时,如果我们试图从netns0命名空间访问任何其他地址,都会失败:


# Inside root namespace$ ip addr show dev eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute eth0       valid_lft 84057sec preferred_lft 84057sec    inet6 fe80::5054:ff:fee3:2777/64 scope link       valid_lft forever preferred_lft forever
# Remember this 10.0.2.15
$ sudo nsenter --net=/var/run/netns/netns0
# Try host's eth0$ ping 10.0.2.15connect: Network is unreachable
# Try something from the Internet$ ping 8.8.8.8connect: Network is unreachable
复制代码


不过,这很容易解释。对于这样的数据包,在netns0的路由表中没有路由。其中,唯一的条目显示了如何到达172.18.0.0/16网络:


# From `netns0` namespace:$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10
复制代码


Linux 有很多方法来填充路由表。其中之一是从直接连接的网络接口提取路由。记住,在命名空间创建后,netns0的路由表是空的。但随后我们添加了ceth0设备,并为它分配了一个 IP 地址172.18.0.10/16。由于我们使用的不是一个简单的 IP 地址,而是地址和网络掩码的组合,网络堆栈会设法从中提取路由信息。每个发往172.18.0.0/16网络的数据包将通过ceth0设备发送。但是任何其他的包都会被丢弃。类似地,在根命名空间中有一条新路由:


# From `root` namespace:$ ip route# ... omited lines ...172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11
复制代码


现在,我们已经回答了我们的第一个问题。我们现在知道了如何隔离、虚拟化和连接 Linux 网络堆栈。

通过虚拟网络交换机(网桥)实现容器互连

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


# From root namespace$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name 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 link set lo up$ ip link set ceth1 up$ ip addr add 172.18.0.20/16 dev ceth1
复制代码


我最喜欢的部分,检查连接:


# From `netns1` we cannot reach the root namespace!$ ping -c 2 172.18.0.21PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.From 172.18.0.20 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.20 icmp_seq=2 Destination Host Unreachable
--- 172.18.0.21 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 55mspipe 2
# But there is a route!$ ip route172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20
# Leaving `netns1`$ exit
# From root namespace we cannot reach the `netns1`$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.11 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.11 icmp_seq=2 Destination Host Unreachable
--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 23mspipe 2
# From `netns0` we CAN reach `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) bytes of data.64 bytes from 172.18.0.21: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.21: icmp_seq=2 ttl=64 time=0.046 ms
--- 172.18.0.21 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 33msrtt min/avg/max/mdev = 0.037/0.041/0.046/0.007 ms
# But we still cannot reach `netns1`$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.10 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.10 icmp_seq=2 Destination Host Unreachable
--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 63mspipe 2
复制代码


有点不对劲……netns1遇到问题。由于某些原因,它不能与根通信,我们也不能从根命名空间访问它。然而,由于两个容器都位于同一个 IP 网络 172.18.0.0/16 中,我们现在可以从netns0容器与主机的veth1进行通信。非常有趣……


我花了些时间才想明白,但显然我们面临的是路由冲突。让我们检查下根命名空间中的路由表:


$ ip route# ... omited lines ... #172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21
复制代码


虽然在添加了第二个veth对后,根的网络堆栈学习到了新的路由172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21,但是,现有的路由中已经有一条针对同一网络的路由。当第二个容器试图 pingveth1设备时,将选择第一个路由,这会破坏连接。如果我们删除第一条路由sudo ip route delete 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11,并重新检查连接,情况就会反过来,即netns1的连接将恢复,但netns0就有问题了。



我相信,如果我们为netns1选择另一个 IP 网络,一切就没问题了。然而,多个容器位于一个 IP 网络中是一个合理的用例。因此,我们需要以某种方式调整veth方法…


看看 Linux 网桥——另一种虚拟网络设施!Linux 网桥的行为就像一个网络交换机。它会在连接到它的接口之间转发数据包。因为它是一个交换机,所以它是在 L2(即以太网)层完成这项工作的。


让我们试着操作下吧。但首先,我们需要清理现有的设置,因为到目前为止,我们所做的一些配置更改实际上已经不再需要了。删除网络命名空间就足够了:


$ sudo ip netns delete netns0$ sudo ip netns delete netns1
# But if you still have some leftovers...$ sudo ip link delete veth0$ sudo ip link delete ceth0$ sudo ip link delete veth1$ sudo ip link delete ceth1
复制代码


快速重建两个容器。注意,我们没有给新的veth0veth1设备分配任何 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 netns netns0
$ 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$ exit
$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name ceth1$ sudo ip link set veth1 up$ sudo ip link set ceth1 netns netns1
$ 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$ exit
复制代码


确保主机上没有新路由:


$ ip routedefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
复制代码


最后,创建网桥接口:


$ sudo ip link add br0 type bridge$ sudo ip link set br0 up
复制代码


现在,将veth0veth1两端都连接到网桥上:


$ sudo ip link set veth0 master br0$ sudo ip link set veth1 master 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) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.259 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.051 ms
--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 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) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.089 ms
--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 36msrtt min/avg/max/mdev = 0.037/0.063/0.089/0.026 ms
复制代码


真令人愉快!一切正常。使用这种新方法,我们根本没有配置veth0veth1。我们只在ceth0ceth1端分配了两个 IP 地址。但是,由于它们都在同一个以太网段(记住,我们将它们连接到虚拟交换机),所以 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
复制代码


恭喜,我们学会了如何将容器变成友好的邻居,防止它们相互干扰,并保持连接性。

访问外部世界(IP 路由和伪装)

容器之间可以通信了。但它们可以和主机(即根命名空间)通信吗?


$ sudo nsenter --net=/var/run/netns/netns0$ ping 10.0.2.15  # eth0 addressconnect: Network is unreachable
复制代码


很明显,netns0中没有相应的路由:


$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10
复制代码


根命名空间也不能和容器通信:


# Use exit to leave `netns0` first:$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable
--- 172.18.0.10 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms
$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable
--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms
复制代码


为了在根命名空间和容器命名空间之间建立连接,我们需要为网桥网络接口分配 IP 地址:


$ sudo ip addr add 172.18.0.1/16 dev br0
复制代码


一旦我们给网桥接口分配了 IP 地址,我们的主机路由表上就会多一条路由:


$ ip route# ... omitted lines ...172.18.0.0/16 dev br0 proto kernel scope link src 172.18.0.1
$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.049 ms
--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 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) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.059 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.056 ms
--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 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 add default via 172.18.0.1$ ping -c 2 10.0.2.15PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 10.0.2.15: icmp_seq=2 ttl=64 time=0.053 ms
--- 10.0.2.15 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 14msrtt min/avg/max/mdev = 0.036/0.044/0.053/0.010 ms
# And repeat the change for `netns1`
复制代码


这项更改基本上把主机变成了路由器,网桥接口成了容器的默认网关。



很好,我们将容器与根命名空间连接起来了。现在,让我们尝试将它们与外部世界连接起来。默认情况下,在 Linux 中数据包转发(即路由器功能)是禁用的。我们需要打开它:


# In the root namespacesudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
复制代码


又到我最喜欢的部分了,检查连接:


$ sudo nsenter --net=/var/run/netns/netns0$ ping 8.8.8.8# hangs indefinitely long for me...
复制代码


还是不行。我们漏了什么吗?如果容器向外部世界发送数据包,那么目标服务器将不能将数据包发送回容器,因为容器的 IP 地址是私有的。也就是说,只有本地网络才知道特定 IP 的路由规则。世界上有很多容器共享完全相同的私有 IP 地址172.18.0.10


解决这个问题的方法叫做网络地址转换(NAT)。在进入外部网络前,由容器发出的数据包将其源 IP 地址替换为主机的外部接口地址。主机还将跟踪所有现有的映射,并且在数据包到达时,它会在将其转发回容器之前还原 IP 地址。听起来很复杂,但我有个好消息要告诉你!有了iptables模块,我们只需要一个命令就可以实现:


$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE
复制代码


这个命令相当简单。我们正在向POSTROUTING链的nat表添加一条新规则,要求伪装所有源自172.18.0.0/16网络的数据包,但不是通过网桥接口。检查连接:


$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 8.8.8.8PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms
--- 8.8.8.8 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms
复制代码


注意,我们遵循的是默认允许(by default - allow)策略,这在现实世界中可能相当危险。对于每个链,主机默认的 iptables 策略都是ACCEPT


sudo iptables -S-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT
复制代码


相反,作为一个很好的例子,Docker 默认限制了一切,然后只启用已知路径的路由。以下是在 CentOS 8 机器上(在 5005 端口上暴露了单个容器)Docker 守护进程生成的转储规则:


$ sudo iptables -t filter --list-rules-P INPUT ACCEPT-P FORWARD DROP-P OUTPUT ACCEPT-N DOCKER-N DOCKER-ISOLATION-STAGE-1-N DOCKER-ISOLATION-STAGE-2-N DOCKER-USER-A FORWARD -j DOCKER-USER-A FORWARD -j DOCKER-ISOLATION-STAGE-1-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT-A FORWARD -o docker0 -j DOCKER-A FORWARD -i docker0 ! -o docker0 -j ACCEPT-A FORWARD -i docker0 -o docker0 -j ACCEPT-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5000 -j ACCEPT-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2-A DOCKER-ISOLATION-STAGE-1 -j RETURN-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP-A DOCKER-ISOLATION-STAGE-2 -j RETURN-A DOCKER-USER -j RETURN
$ sudo iptables -t nat --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P POSTROUTING ACCEPT-P OUTPUT ACCEPT-N DOCKER-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 5000 -j MASQUERADE-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER-A DOCKER -i docker0 -j RETURN-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5005 -j DNAT --to-destination 172.17.0.2:5000
$ sudo iptables -t mangle --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT-P POSTROUTING ACCEPT
$ sudo iptables -t raw --list-rules-P PREROUTING ACCEPT-P OUTPUT ACCEPT
复制代码

从外部访问容器(端口发布)

我们都知道,有一种做法是将容器端口发布到主机的部分(或全部)接口。但端口发布的真正含义是什么?


假设我们有一个在容器内运行的服务器:


$ sudo nsenter --net=/var/run/netns/netns0$ python3 -m http.server --bind 172.18.0.10 5000
复制代码


如果我们试图从主机向这个服务器进程发送一个 HTTP 请求,一切都没问题(好吧,根命名空间和所有容器接口之间都有连接,为什么没有呢?):


# From root namespace$ curl 172.18.0.10:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omited lines ...
复制代码


但是,如果我们要从外部访问该服务器,我们将使用哪个 IP 地址?我们知道的唯一 IP 地址可能是主机的外部接口地址eth0


$ curl 10.0.2.15:5000curl: (7) Failed to connect to 10.0.2.15 port 5000: Connection refused
复制代码


因此,我们需要找到一种方法,将任何到达主机eth0接口 5000 端口的数据包转发到目的地172.18.0.10:5000。或者,换句话说,我们需要在主机的eth0接口上发布容器的 5000 端口。iptables 拯救了我们!


# External trafficsudo iptables -t nat -A PREROUTING -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000# Local traffic (since it doesn't pass the PREROUTING chain)sudo iptables -t nat -A OUTPUT -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000
复制代码


此外,我们需要启用iptables拦截桥接网络上的流量


sudo modprobe br_netfilter
复制代码


测试时间!


curl 10.0.2.15:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omited lines ...
复制代码

理解 Docker 网络驱动

好的,先生,我们能用这些无用的知识做什么呢?例如,我们可以试着理解一些Docker网络模式


让我们从--network host模式开始。试着比较下命令ip linksudo docker run -it——rm——network host alpine ip link的输出。想不到,它们居然一模一样!即在host模式下,Docker 不使用网络命名空间隔离,容器工作在根网络命名空间中,并与主机共享网络堆栈。


下一个模式是--network nonesudo docker run -it --rm --network none alpine ip link命令只显示了一个环回网络接口。这与我们对新创建的网络命名空间的观察非常相似。也就是在我们添加任何veth设备之前。


最后但同样重要的是--network bridge(默认)模式。这正是我们在整篇文章中试图再现的。我建议你试用下ipiptables命令,并从主机和容器的角度检查网络堆栈。

附:无根容器和网络

podman容器管理器的一个很好的特性是针对无根容器的。然而,你可能已经注意到,我们在本文中使用了大量sudo升级。换句话说,权限就不可能配置网络。Podman的rootfull网络方法和 docker 非常接近。但是当涉及到无根容器时,podman 依赖于slirp4netns项目:


从 Linux 3.8 开始,非特权用户可以创建 network_namespaces(7)和 user_namespaces(7)了。但是,非特权网络命名空间并不是很有用,因为在主机和网络命名空间之间创建 veth(4)对仍然需要 root 特权。(即没有网络连接)


通过将网络命名空间中的 TAP 设备连接到用户模式 TCP/IP 堆栈(“slirp”),slirp4netns 允许以完全非特权的方式将网络命名空间连接到网络。


无根网络有很大的局限性:“从技术上讲,容器本身没有 IP 地址,因为没有根权限,网络设备关联就无法实现。此外,无根容器无法 ping,因为它缺少 ping 命令所需的 CAP_NET_RAW 安全能力。”但这总比完全没有连接好。

小结

本文探讨的组织容器网络的方法只是其中一种可能的方法(可能是使用最广泛的一种)。还有很多其他的方法,通过官方或第三方插件实现,但它们都严重依赖于Linux网络可视化工具。因此,容器化可以被视为虚拟化技术。


原文链接:


https://iximiuz.com/en/posts/container-networking-is-simple/

2021 年 2 月 26 日 10:532494
用户头像

发布了 351 篇内容, 共 151.9 次阅读, 收获喜欢 772 次。

关注

评论

发布
暂无评论
发现更多内容

它们为什么这么快:从多进程到多线程再到I/O复用

Ya

多线程 进程 并发

区块链系列教程之:比特币的问题

程序那些事

比特币 区块链 智能合约 以太坊

一致性哈希 -- java 实现

lei Shi

谈谈Spring xml配置文件中的命名空间,以及一些例外情况

xiaoxi666

spring 命名空间

刚去面试现场聊了一个多小时的Redis ,悄悄分享给大家!

Java小咖秀

nosql redis Java 面试

架构训练营第五周 - 总结

无心水

极客大学架构师训练营

【Python】 any() 和 or 区别你真的知道吗?

Leetao

Python 数据结构 Python基础知识

针对GPU单指令多数据流的编译优化算法

GPU

gpu 编译器 程序语言 if-conversion

GeekPwn 2020少年黑客马拉松大赛即将开启 谁将CARRY全场?

Geek_116789

Git 的远端操作及解析(含思维导图)

多选参数

git GitHub gitlab

公司一直用Mybatis的原因原来在这!不得不竖起我的大拇指

小闫

Java mybatis mybatis-config.xml mybatis缓存

从Servlet到Spring Boot

废材姑娘

Java Spring Boot

架构训练营第五周 - 作业

无心水

极客大学架构师训练营

Raft探索历程--Part2

老胡爱分享

分布式系统 raft

依赖倒置原则

John

极客大学架构师训练营

面试官:为什么需要happens-before规则和什么是指令重排序

无予且行

Java 编程 程序员 happens-before java面试

week5.课后作业

个人练习生niki

ARTS打卡 第6周

引花眠

ARTS 打卡计划

架构师训练营 第5课学习总结

Glowry

极客大学架构师训练营

架构师训练营学习总结

John

极客大学架构师训练营

Week3:作业二

车小勺的男神

重学 Java 设计模式:实战策略模式「模拟多种营销类型优惠券,折扣金额计算策略场景」

小傅哥

Java 设计模式 小傅哥 重构 代码优化

小白也有大厂梦,如何从零开始掌握高薪Java工程师必备技能?

无予且行

Java 架构 面试 后端 大厂

三十张图助你看清红黑树的前世今生

淡蓝色

Java 程序员 数据结构 算法

有了多线程,为什么还要有协程?

八两

线程 进程 协程 GMP 进程线程区别

操作系统概览

引花眠

计算机基础

分布式缓存架构与负载均衡架构

负载均衡 极客大学架构师训练营 消息队列 分布式缓存 第五周

架构师训练营 第5课作业

Glowry

极客大学架构师训练营

视读——沟通的艺术,看入人里,看出人外(开篇)

废材姑娘

读书笔记 视觉笔记

[1.3万字] 玩转前端二进制

阿宝哥

Java 前端 base64 Blob

现在面试这么难,背下题就能过的时代一去不复返了

小谈

Java JVM Java 面试 springboot SpringCloud

Hummer 轻量级跨端技术框架详解及实战

Hummer 轻量级跨端技术框架详解及实战

手把手带你搞定4大容器网络问题-InfoQ