资讯专栏INFORMATION COLUMN

CloudBest:干货 | 手把手带你搞定4大容器网络问题

Tecode / 1772人阅读

摘要:一直以来,网络都是容器中令人头疼的问题。本文的主要目的是带你解决容器网络问题,让你不再对它恐惧。或者,更准确地说,是单主机容器网络问题。与其创建完全隔离的容器,不如将范围限制在网络堆栈中。

一直以来,网络都是容器中令人头疼的问题。本文的主要目的是带你解决容器网络问题,让你不再对它恐惧。

使用容器总是感觉像变魔术一样。对那些了解其内部原理的人来说,它是一种很好的方式;而对那些不了解其内部原理的人来说,这是一种可怕的方式。幸运的是,我们研究容器化技术的内部原理已经很长一段时间了。我们甚至发现,容器只是隔离的、受限制的 Linux 进程,镜像并不是运行容器所必须的,相反——要构建一个镜像,我们需要运行一些容器。

image.png

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

  • 如何虚拟化网络资源,使容器认为它们中的每一个都有一个专用的网络堆栈?
  • 如何将容器变成友好的邻居,防止它们相互干扰,并教它们如何很好地沟通?
  • 怎样从容器内部访问外部世界(比如互联网)?
  • 如何从外部世界(即端口发布)访问运行在一台机器上的容器?

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

  • 网络命名空间
  • 虚拟以太网设备(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)。相反,我们将关注基本概念,并使用最简单的工具来实现我们的学习目标。

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

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

我们可以快速创建一个不是很完善的inspect-net-stack.sh脚本:

#!/usr/bin/env bash

echo "> Network devices"
ip link

echo -e "
> Route table"
ip route

echo -e "
> Iptables rules"
iptables --list-rules

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

$ sudo iptables -N ROOT_NS

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

$ sudo ./inspect-net-stack.sh
> Network devices
1: lo:  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:  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

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

$ sudo nsenter --net=/var/run/netns/netns0 bash
# The newly created bash process lives in netns0

$ sudo ./inspect-net-stack.sh
> Network devices
1: lo:  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 链,只有一个环回网络设备。到目前为止,一切顺利……

image.png

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

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

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

$ sudo ip link add veth0 type veth peer name ceth0

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

$ ip link
1: lo:  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:  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:  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:  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

创建后,veth0和ceth0都驻留在主机的网络堆栈(也称为根网络命名空间)上。为了连接根命名空间和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 link
1: lo:  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:  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
6: veth0@if5:  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 link
1: lo:  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
5: ceth0@if6:  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

image.png

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

现在可以检查下连接了:

# From `netns0` ping roots veth0
$ ping -c 2 172.18.0.11
PING 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 ms
64 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 58ms
rtt 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.10
PING 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 ms
64 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 3ms
rtt min/avg/max/mdev = 0.046/0.059/0.073/0.015 ms

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

# Inside root namespace
$ ip addr show dev eth0
2: eth0:  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 hosts eth0
$ ping 10.0.2.15
connect: Network is unreachable

# Try something from the Internet
$ ping 8.8.8.8
connect: Network is unreachable

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

# From `netns0` namespace:
$ ip route
172.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.21
PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.
From 172.18.0.20 icmp_seq=1 Destination Host Unreachable
From 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 55ms
pipe 2

# But there is a route!
$ ip route
172.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.20
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.
From 172.18.0.11 icmp_seq=1 Destination Host Unreachable
From 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 23ms
pipe 2

# From `netns0` we CAN reach `veth1`
$ sudo nsenter --net=/var/run/netns/netns0
$ ping -c 2 172.18.0.21
PING 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 ms
64 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 33ms
rtt 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.20
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.
From 172.18.0.10 icmp_seq=1 Destination Host Unreachable
From 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 63ms
pipe 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.11
172.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就有问题了。

image.png

我相信,如果我们为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

快速重建两个容器。注意,我们没有给新的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 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 route
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

最后,创建网桥接口:

$ sudo ip link add br0 type bridge
$ sudo ip link set br0 up

现在,将veth0和veth1两端都连接到网桥上:

$ sudo ip link set veth0 master br0
$ sudo ip link set veth1 master br0

image.png

然后检查容器之间的连接:

$ sudo nsenter --net=/var/run/netns/netns0
$ ping -c 2 172.18.0.20
PING 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 ms
64 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 2ms
rtt 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.10
PING 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 ms
64 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 36ms
rtt min/avg/max/mdev = 0.037/0.063/0.089/0.026 ms

真令人愉快!一切正常。使用这种新方法,我们根本没有配置veth0和veth1。我们只在ceth0和ceth1端分配了两个 IP 地址。但是,由于它们都在同一个以太网段(记住,我们将它们连接到虚拟交换机),所以 L2 层上有连接:

$ sudo nsenter --net=/var/run/netns/netns0
$ ip neigh
172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de STALE
$ exit

$ sudo nsenter --net=/var/run/netns/netns1
$ ip neigh
172.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 address
connect: Network is unreachable

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

$ ip route
172.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.10
PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.
From 213.51.1.123 icmp_seq=1 Destination Net Unreachable
From 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.20
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.
From 213.51.1.123 icmp_seq=1 Destination Net Unreachable
From 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.10
PING 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 ms
64 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 11ms
rtt min/avg/max/mdev = 0.036/0.042/0.049/0.009 ms

$ ping -c 2 172.18.0.20
PING 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 ms
64 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 4ms
rtt 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.15
PING 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 ms
64 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 14ms
rtt min/avg/max/mdev = 0.036/0.044/0.053/0.010 ms

# And repeat the change for `netns1`

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

image.png

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

# In the root namespace
sudo 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.8
PING 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 ms
64 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 2ms
rtt 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 RELATEDESTABLISHED -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

# ... omited lines ...

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

$ curl 10.0.2.15:5000
curl: (7) Failed to connect to 10.0.2.15 port 5000: Connection refused

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

# External traffic
sudo 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 doesnt 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


# ... omited lines ...

理解 Docker 网络驱动

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

https://docs.docker.com/network/#network-drivers

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

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

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

附:无根容器和网络

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

https://www.redhat.com/sysadmin/container-networking-podman

但是当涉及到无根容器时,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 安全能力。”但这总比完全没有连接好。

https://www.redhat.com/sysadmin/container-networking-podman

小结

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/126125.html

相关文章

  • #yyds干货盘点#学不懂Netty?看不懂源码?不存在的,这篇文章把手带你阅读Netty源码

    摘要:简单来说就是把注册的动作异步化,当异步执行结束后会把执行结果回填到中抽象类一般就是公共逻辑的处理,而这里的处理主要就是针对一些参数的判断,判断完了之后再调用方法。 阅读这篇文章之前,建议先阅读和这篇文章关联的内容。 1. 详细剖析分布式微服务架构下网络通信的底层实现原理(图解) 2. (年薪60W的技巧)工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)...

    zsirfs 评论0 收藏0
  • CloudBest:年度复盘丨盘点2020无处不在的「云原生」

    摘要:华为云华为云在云原生这场游戏中,最具竞争力的玩家之一。年,金山云在云原生领域推出了三款重磅产品星曜裸金属服务器云服务器和云盘。在线上智博会上,浪潮云发布了经过全新迭代升级的浪潮云,进一步提升平台云原生服务能力。面对数字时代复杂系统的不确定性,传统的 IT 应用架构研发交付周期长、维护成本高、创新升级难,烟囱式架构,开放性差、组件复用度低,这些都成为了企业业务快速增长的瓶颈。而云原生以其敏捷、...

    Tecode 评论0 收藏0
  • Java后端

    摘要:,面向切面编程,中最主要的是用于事务方面的使用。目标达成后还会有去构建微服务,希望大家多多支持。原文地址手把手教程优雅的应用四手把手实现后端搭建第四期 SpringMVC 干货系列:从零搭建 SpringMVC+mybatis(四):Spring 两大核心之 AOP 学习 | 掘金技术征文 原本地址:SpringMVC 干货系列:从零搭建 SpringMVC+mybatis(四):Sp...

    joyvw 评论0 收藏0
  • Android自定义控件

    摘要:自定义之高仿支付宝芝麻信用分仪表盘效果最近写的一个自定义,仿了下支付宝新旧版的芝麻信用分的仪表盘效果。项目需求讨论自定义实现步骤及封装根据实际项目需求出发。来快速实现各种自定义界面的对话框。究其原因,目前只用到了最基础的一部分功能。 自定义 View 之高仿支付宝芝麻信用分仪表盘效果 最近写的一个自定义 View,仿了下支付宝新旧版的芝麻信用分的仪表盘效果。 项目需求讨论 - Andr...

    Tikitoo 评论0 收藏0
  • 【 全干货 】5 分钟带你看懂 Docker !

    摘要:本文从定义,作用,技术架构,安装和使用等全方位带你看懂。如图中左边红框中和右边的红框中都唯一表示为同一个镜像。最后,于开发者而言提供了一种开发环境的管理办法,与测试人员而言保证了环境的同步,于运维人员提供了可移植的标准化部署流程。 作者丨唐文广:腾讯工程师,负责无线研发部地图测试。 导语:Docker,近两年才流行起来的超轻量级虚拟机,它可以让你轻松完成持续集成、自动交付、自动部署...

    Edison 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<