容器技术是最近几年最流行的技术之一,将程序所依赖的环境打包成一个镜像文件,并可以跨平台部署,真正做到了一次编译,处处运行,对研发和运维体系都产生了巨大的影响。
01 开篇
某天一元戎网络萌新正在学习Docker容器,他看见书上有段这样描述:
通过Docker镜像可以将运行环境固化,但最终运行的时候还需要运行时的隔离技术,一种是空间的隔离,另一种是资源的隔离。先说一下空间的隔离,每一个容器都有自己独立的命名空间,主要包括网络命名空间......
萌新:“嗯?是对每个容器的网络环境进行隔离吗?是怎么实现的呢?”
萌新思索一阵子后——萌新:“算了,想不明白,找波哥问问去”
02 Docker的网络方案
此时的波哥正在家里悠闲逛着Stack Overflow,突然电话响起。
萌新:“喂!波哥,在干嘛呢?问你些小问题。”
波哥:“正在养生冲浪呢,啥问题你说。”
萌新:“就想找你了解下Docker的网络方案,我家断网了!”
波哥:“好吧,那我给你讲讲吧!”
Docker的默认网络模式可以分为:Host 模式、Bridge 模式或者 None 模式。
Host 模式就是和宿主机共享协议栈,那么就可以在容器里面看到宿主机的网络 IP 等信息,可以通过 localhost 访问宿主机上面的服务。这里需要注意下,在容器内启动服务需要避免和宿主机的端口冲突。
None 模式是不连接网络的,这个主要有两个用途——
a. 是有些业务场景,容器是不需要联网的,例如一些本地批处理任务等;
b. 是可以让用户自己添加网络,用户可以通过 ovs-docker 等工具为容器自定义网卡。
而 Bridge 模式是Docker的默认网络模式,也是最常用的模式,这种模式下的容器会被分配一个172.17.0.0/16网段的IP。并且容器和主机/其他Bridge模式容器可以互相访问,容器也能访问外网,但是外网不能直接访问容器。
萌新:“那Bridge模式是怎么实现的呢?”
03背景知识
波哥:“你了解过Network Namespace、veth、linux的bridge模块、Netfilter...吗?”
萌新:“没有诶,这是啥?”
波哥:“好吧,那给你讲Bridge模式实现原理前,得先给简单介绍下这几个前置知识点啦!”
Network NamespaceNetwork
Namespace 是linux内核提供的一种资源隔离机制,它能创建多个隔离的网络空间,它们有独自的网络栈信息。不管是虚拟机还是容器,运行的时候仿佛自己就在独立的网络中。
我们可以使用 ip netns 去管理namespace,例如可以使用 ip netns add 去创建一个namespace,被创建的network namespace 会出现在 /var/run/netns 下;
使用 ip netns ls 查看 /var/run/netns 下现有的namespace,如果需要管理其他不是 ip netns 创建的namespace,只要在这个目录下创建一个指向network namespace文件的链接就行。
(例如你会发现容器所在的network namespace就无法直接管理,如果想知道如何去管理容器的network namespace 请继续往后看)...
Veth
有了不同 Network Namespace 之后,也就有了网络的隔离,但是如果它们之间没有办法通信,也没有实际用处。那不同的Network Namespace之间是如何通信的呢?
Linux内核也提供了一种特殊的网络接口设备:Veth,Veth接口是成对创建,在一端发出的报文会在另一端收到,相当于一根网线的两端。那么就可以把一对Veth分别放在两个Network Namespace中,通过报文转发来实现不同Network Namespace之间的通信。
Linux的Bridge模块
Veth pair 可以实现两个Network Namespace之间的通信,但是当存在多个Network Namespace需要通信时,它就只能干看着了,等待一个linux中叫做Bridge的大哥去解决了。
Linux Bridge(网桥)是工作于二层的虚拟网络设备,功能类似于物理的交换机。它是一类网络接口,可以把其他网络接口加入到bridge接口中。同属一个Bridge的接口相当于接入同一个二层交换机,可以进行二层报文的转发。假设创建了一个Bridge接口br0,使用veth pair把eth0、eth1、eth2和br0连接起来,那么eth0收到的报文不会进入协议栈,而是在br0内进行报文转发,因此加入到Bridge内的接口所设置的ip是不起作用的,而br0接口收到的报文是会进入协议栈的,因此可以设置br0的ip地址。
Netfilter
了解了上述内容,我们已经大概知道了不同Network Namespace大致的通信原理。那它们访问外网呢?这里我们还需要去了解一套linux内核机制 -- Netfilter。
Netfilter是linux内核中的对报文转发进行控制的一套机制,能在报文转发路径上不同的时间点对报文进行不同的控制。时间点包括 接收报文进入协议栈进行路由前(NF_IP_PRE_ROUTING) 、 接收报文路由结果是本地(NF_IP_LOCAL_IN) 、 接收报文路由结果是转发(NF_IP_FORWARD) 、 本机发送的报文进行路由之前(NF_IP_LOCAL_OUT) 、 路由后要离开本机的报文(NF_IP_POST_ROUTING) ,对报文的控制包括转发,丢弃,nat地址转换,修改报文等行为。Netfilter的功能十分复杂和强大,能对报文转发实现灵活的控制,是Linux下很多防火墙实现的基础。
04Bridge
模式实现原理萌新:“停!停!停!我是要了解Docker的Bridge模式实现,你怎么给我扯防火墙啊!”
波哥:“你别急啊!前面不是知识铺垫嘛,这不就准备给你讲Bridge模式的实现啦~”
在Docker的Bridge网络模式下,首先要做到的就是隔离,Docker会为每个容器创建一个Network Namespace,容器中的进程都运行在容器所属的namespace中,因此容器内进程的网络资源和外部是隔离的。
萌新:“那怎么查看一个容器的所属的namespace呢?和这个namespace的一些信息呢?”
波哥:“好问题!为了方便给你演示,咱们共享下屏幕吧!我操作给你看,语音说不清!”
萌新:“好的!你开在线会议吧!”
波哥:“???你不是没网吗?”(无奈于萌新
为了方便演示我本地先启动了一个etcd容器,容器 name 为 etcd_test
(点击查看大图)
接下来我们便可以去实操查看这个容器的namespace啦。我们首先使用 docker inspect 命令去查看容器的Pid,可以看见Pid为 7236
紧接着我们可以使用命令 sudo ls -l /proc/7236/ns 查看容器的Network Namespace 所对应的文件号,所获取的内容种net所对应的值便是所需的namespace的文件号 4026532644
拿到文件号后,我们便可以直接去 /var/run/docker/netns 路径中找到所对应namespace了,其中 544777b0b671 便是容器 etcd_test 的namespace了
因为我们前面所说的 ip netns 只能管理位于 /var/run/netns 下的namespace,为此我们需要使用sudo ln -s /proc/7236/ns/net /var/run/netns/544777b0b671将namespace链过去,这样我们就可以使用 ip netns 去管理这个namespace了
容器内的eth0实际是一个veth接口,该接口的另一端在宿主机的Network namespace中,可以通过ip命令查看,接口名后面的@if14表示该接口的另一端ifindex为14,因此可以看到,容器的eth0和宿主机的 veth0fc045f 是一对veth接口,并且还能看到veth0fc045f 信息中有 master docker0,说明 veth0fc045f 是属于docker0的接口。
docker0接口是docker server创建的一个bridge接口,通过把veth在宿主机的一端加入到docker0,实现容器与宿主机/其他容器的二层通信,可以看到docker0的ip为172.17.0.1,和容器的ip是同一个网段,因此可以通过docker0和容器进行通信。
我们也可以通过 brctl 来查看bridge的相关信息,可以看见veth0fc045f 是属于docker0的接口。
到这里我们已经知道了容器之间是怎么访问的,那容器和外网呢?docker创建的容器默认是可以访问外网的,但是容器的ip是一个私有网段的ip,那么容器是如何和外网ip进行通信呢?答案是nat地址转换。我们使用 iptables 查看下nat表:
(点击查看大图)
会发现docker server创建了一条netfilter规则: -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE` ,它的意思是源地址是172.17.0.0/16并且不是要转发到docker0的报文,在路由之后,把源地址替换成出接口地址。
前文提到的docker0接口充当了网关的作用,容器内设置默认网关为docker0的地址(172.17.0.1),当访问外网IP的时候,报文首先到达宿主机接口docker0,此时netfilter的规则生效,把报文的源地址改写为出接口ip,报文相当于由宿主机发送到外网ip,当收到外网发回来的报文的时候,再把目的地址转换为源地址。从而实现了容器访问外网。
那外网是如何去访问容器的呢?
通常在创建容器的时候设置端口映射,把对宿主机指定端口的访问转发到容器的指定端口,对外暴露宿主机的ip和端口。正如我们的 etcd_test 容器,便是对宿主机的2380、2379端口和容器的2380、2379做了映射。
在上面的查询的filter和nat表中,你也可以发现这样的两条规则:1. filter 表:控制数据包是否允许进出及转发,可以控制的链路有 INPUT、FORWARD 和 OUTPUT。2. nat 表:控制数据包中地址转换,可以控制的链路有 PREROUTING、INPUT、OUTPUT 和 POSTROUTING。
在创建带端口映射的容器的时候,docker为每对端口映射添加了三条规则,拿 etcd_test 容器的2380端口举例:
为此整个外部访问容器的流程是酱紫的:
- 外部访问2380端口的报文,首先在进入路由前由 -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER 规则命中,转到DOCKER链中处理;
- 在DOCKER链中命中规则 -A DOCKER ! -i docker0 -p tcp -m tcp --dport 2380 -j DNAT --to-destination 172.17.0.2:2380 ,报文的目的ip和端口改为172.17.0.2:2380;
- 报文继续走路由流程,路由结果表示出接口是docker0,于是命中 -A FORWARD -o docker0 -j DOCKER 规则,进入DOCKER链;
- 在DOCKER链中,命中规则 -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 2380 -j ACCEPT ,接受报文,报文进入docker0,目的mac修改为通过arp获得172.17.0.2对应的mac地址(veth的mac),按照bridge的二层转发过程,把报文送入docker0下属的指定接口(也就是前文提到的容器eth对应的veth接口);
- 按照veth的特性,容器内的eth0收到了报文,报文进入容器的协议栈;
- 容器响应的报文根据前面设置的DNAT和CONNTRACK,把源ip和端口改为宿主机出口的ip和端口,往外发送。