虚拟技术和云飞速发展的今天,云和容器已经深入人心,每个IT人都或多或少的使用容器和云。但是用归用,很多人对其底层的原理确实知之甚少。很多人想把容器当成虚拟机来用,却遭遇大大的阻碍,如果理解容器原理的话,你就会发现容器实际上是孤立且受限制的linux进程,和其他进程也一样。运行容器实际上并不需要镜像,相反,要构建镜像,则必须要运行一些容器。但是不敢怎么着,容器是要用来使用的,必然要联网的,所以容器联网的原理是有限需要了解的,为此本文我们就来一起学习容器的网络原理。
大家可能都知道Linux网络实际上是在内存空间的堆栈数据,包括Linux网络设备的集合、路由规则、netfilter hook集等(包括iptables规则定义的)。为了获取Linux的这些网络堆栈数据,需要用到几个工具,包括netstat –ie ,ifconfig,ip link,ip route和iptables –list-rules等。
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:2a:77 brd ff:ff:ff:ff:ff:ff
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
iptables –list-rules
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
用于容器隔离的Linux命名空间之一称为网络命名空间。从逻辑上讲,网络命名空间是网络堆栈的另一个副本,有其自己的路由,防火墙规则和网络设备。为了简单起见,这是我们将在本文中使用的唯一命名空间。与其创建完全隔离的容器,不如将范围限制为仅网络堆栈。ip工具在现在OS默认自带的iproute2集合的一部分,这些工具可以用来创建网络命名空间。
sudo ip netns add netns0
ip netns
netns0
为了使用这个刚新建的的命名空间netns0,可以使用Linux命令称nsenter。它输入一个或多个指定的命名空间,然后执行给定的程序:
sudo nsenter --net=/var/run/netns/netns0 bash
ip link
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
从上面的输出中可以明显看出,在命名空间内运行的bash进程netns0看到了完全不同的网络堆栈。没有路由规则,也没有自定义iptables链,只有一个环回网络设备。
如果不能与专用网络堆栈通信,那么容器也就没啥用。为此Linux提供了合适的工具,虚拟以太网设备veth,这个设备是虚拟以太网设备,可以用来充当网络命名空间之间的隧道,以创建到另一个命名空间中的物理网络设备的桥梁,但也可以用作独立的网络设备。”
虚拟以太网设备需要成对使用,用下面命令创建一组互连的虚拟以太网设备veth0和ceth0:
sudo ip link add veth0 type veth peer name 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: <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
双方veth0并ceth0创建所在的主机的网络堆栈后(也称为根网络命名空间)。要将根命名空间与该netns0命名空间连接,需要将其中一台设备保留在根命名空间中,然后将另一台设备移至netns0:
sudo ip link set ceth0 netns netns0
网络信息:
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
6: 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
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 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
5: 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
图示:
为了检查连接性,我们从netns0(172.18.0.11),ping veth0(172.18.0.10)
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
从根命名空间ping ceth0(172.18.0.10)
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命名空间访问其他任何地址是ping不通的。
ip addr show dev eth0
2: 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
sudo nsenter --net=/var/run/netns/netns0
ping 10.0.2.15
connect: Network is unreachable
ping 8.8.8.8
connect: Network is unreachable
这是显而易见的,因为netns0路由表中根本没有这类数据包的路由。唯一的条目显示了如何连接172.18.0.0/16网络:
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设备发送。但是任何其他数据包将被丢弃。同样,根命名空间中新增加了一条路由:
ip route
172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11
我们知道如何隔离、虚拟化和连接Linux网络堆栈。
容器化的整个思想归结为有效的资源共享。也就是说,每台机器显然不可能只有一个容器。相反,会尽最大可能在共享环境中运行更多的容器。试想,按照同样步骤,在上面的基础上,再增加第二个容器:
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
然后,检查连通性:
从netns1 ping 根命名空间:
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
ping不通,看一下路由:
ip route:
172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20
有路有!
从根命名空间ping 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
从netns0可以ping通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
但是,也ping不同通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 ping不同根命名空间。由于两个容器都位于同一IP网络中172.18.0.0/16,所以可以用veth1和netns0容器与主机进行对话。什么导致以上问题呢?对问题出在路由冲突了!
检查根命名空间中的路由表:
ip route
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,但这和已有的路由冲突了。如果删除第一条路由sudo ip route delete 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11并重新检查连接性, netns1将恢复连接的连通性,netns0会处于不确定状态。
线上用上述方法,veth行不通了。这是就需要另外的可行方法了。这方法就是Linux网桥,另外一个虚拟化网络工具。Linux网桥的行为上类似于网络交换机。它在与其连接的接口之间转发数据包。并且由于它是交换机,因此可以在L2(即以太网)级别上进行操作。我们就来尝试网桥来建容器网络,先清空之前的网络命名空间:
sudo ip netns delete netns0
sudo ip netns delete netns1
sudo ip link delete veth0
sudo ip link delete ceth0
sudo ip link delete veth1
sudo ip link delete ceth1
快速重新创建两个容器。
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
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
确保主机上没有新路由:
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到网桥br0:
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.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
OK!一切正常。这种方法可行。由于还没有配置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
sudo nsenter --net=/var/run/netns/netns1
ip neigh
172.18.0.10 dev ceth1 lladdr 66:f3:8c:75:09:29 STALE
现在容器可以互通互联。但是与主机(即根命名空间)还不能连通:
sudo nsenter --net=/var/run/netns/netns0
ping 10.0.2.15 # eth0 address
connect: Network is unreachable
查看路由:
ip route
172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10
根命名空间无法与容器无路由:
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
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
这配置更改基本上将主机变成了路由器,并且网桥接口成为了容器的默认网关,拓扑图示:
容器现在与根命名空间相连,接着我们配置让他们可以对外链接。默认情况下,在Linux中禁用数据包转发(即路由器功能),我们需要打开它:
sudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
然后,检查连接性:
sudo nsenter --net=/var/run/netns/netns0
ping 8.8.8.8
好吧,有问题,仍然不通。由于容器的IP地址是私有的,容器可以数据包发送到外界,但是目标服务器将无法将数据包发送回容器。为了解决此问题,需要进行网络地址转换(NAT)。在进入外部网络之前,由容器发起的数据包将其源IP地址替换为主机的外部接口地址。主机还将跟踪所有现有的映射,并在到达主机时将还原IP地址,然后再将数据包转发回容器。听起来很复杂,但是做起来不难,可以使用iptables进行转化,只需要一个命令即可实现:
sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE
该命令非常简单。在nat表的POSTROUTING链中添加了一条新规则,要求伪装源自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
注意,默认情况会遵循,允许采取策略,请不要实际生产环境中使用,这非常危险。Docker默认情况下限制所有内容,然后仅对已知路径启用路由。
将容器端口发布到主机的某些(或全部)接口是一种已知的做法。但是端口发布的真正含义是什么?
假设有一个在容器中运行的服务器:
sudo nsenter --net=/var/run/netns/netns0
Python3 -m http.server --bind 172.18.0.10 5000
现在用curl尝试从主机向该服务器进程发送HTTP请求:
curl 172.18.0.10:5000
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
但是,要从外部访问该服务器,要怎么访问呢?现在对外唯一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目的地。换句话说,需要在主机的接口上发布容器的端口5000 eth0。这也可以使用iptables。
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
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
Try,again:
curl 10.0.2.15:5000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
ok了
现在,我们再深入一下,了解一些Docker网络模式。首先从--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(默认)模式。这就是本文中我们使用的方法。
本文中我们使用了很多sudo操作,如果没有root特权就无法配置网络。另外一个云原生标准的容器管理器podman,可以作为docker管理器替换。虫虫之前的文章中对它做过专门介绍。podman一个不错的功能之一就是它专注于无root容器。podman建立根网络的方法非常接近docker,为了实现无root容器,podman依赖于slirp4netns项目:
从Linux 3.8开始,非特权用户可以与user_namespaces(7)一起创建network_namespaces(7)。但是,非特权网络命名空间并不是很有用,因为在主机和网络命名空间上创建veth(4)对仍然需要root特权。(即没有互联网连接)
slirp4netns允许通过将网络命名空间中的TAP设备连接到用户模式TCP / IP堆栈(“slirp”),以完全无特权的方式将网络命名空间连接到Internet。
本文中,我们介绍了在容器网络的详细实现细节方法,了解这些细节对我们熟悉容器网络和排查容器网络相关故障时候非常有意义。当然组织容器网络的方法不可能的只有这种方法(当然,可能是使用最广泛的一种方法)。实际上,还有其他很多的实现方式方式,可以通过官方或第三方插件实现,有兴趣可以查看相关的文档。