喜洋洋礼品 發表於 2018-6-25 08:52:00

Docker 网络之理解 bridge 驱动

<p><span style="font-family: Microsoft YaHei; font-size: 15px">笔者在前文《Docker 网络之进阶篇》中介绍了 CNM(Container Network Model),并演示了 bridge 驱动下的 CNM 使用方式。为了深入理解 CNM 及最常用的 bridge 驱动,本文将探索 bridge 驱动的实现机制。</span><br><span style="font-family: Microsoft YaHei; font-size: 15px">说明:本文的演示环境为 ubuntu 16.04。</span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">dokcer0 网桥</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">在 Ubuntn 上安装 docker 后,宿主机上默认被创建了一个名为 docker0 的网卡,其 IP 为 172.17.0.1/16:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623150434206-741806080.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">有了这个网卡,宿主机还会在内核的路由表中添加一条到达相应网络的静态路由记录:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623150508726-1305200394.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">这条路由信息表示所有目的 IP 为 172.17.0.0/16 的数据包都会从 docker0 网卡发出。接下来我们创建一个名为 mycon 的容器,并观察其网络配置:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623150543458-170023440.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">在 mycon 容器内可以看到两块网卡:lo 和 eth0。其中 lo 是容器的回环网卡,eth0 是容器与外界通信的网卡,eth0 的 IP 信息为 172.17.0.2/16,和宿主机上的网卡 bridge0 在同一网段中。查看 mycon 的路由信息:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623150628978-193410433.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">mycon 容器的默认网关正是宿主机的 docker0 网卡。通过 ping 命令测试与外网的连通性,此时容器 mycon 是可以连通外网的,这就说明 mycon 的 eth0 网卡与宿主机的 docker0 网卡是连通的。</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">下面我们来查看宿主机的网络设备:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623150740460-45642787.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">发现多了一个以 "veth" 开头的网卡,这是一个 veth 设备。而 veth 设备总是成对出现的,那么与 veth7537a16 配对的就应该是 mycon 容器中的 eth0 了。既然 mycon 容器中的 eth0 是与 docker0 连通的,那么 veth7537a16 也应该是与 docker0 连通的。因此 <span style="color: rgba(255, 0, 0, 1)"><strong>docker0 并不是一个简单的网卡设备,而是一个网桥</strong></span>!下图展示了 docker bridge 网络模式的拓扑图:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623150811325-1902935985.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">事实上,docker 创建了 docker0 网桥,并以 veth pair 连接各个容器的网络,容器中的数据通过 eth0 发送到 docker0 网桥上,并由 docker0 网桥完成转发。<strong>这里网桥的概念等同于交换机,为连在其上的设备转发数据帧。网桥上的 veth 网卡设备相当于交换机上的端口,可以将多个容器连接在它们上面,这些端口工作在二层</strong>,所以是不需要配置 IP 信息的。上图中的 docker0 网桥就为连在其上的容器转发数据帧,使得同一台宿主机上的 docker 容器之间可以相互通信。既然 docker0 是二层设备,那么它为什么还需要 IP 呢?其实,docker0 是一个普通的 linux 网桥,是可以为它配置 IP 的,我们可以认为它的内部有一个可以用于配置 IP 的网卡。<strong>Docker0 的 IP 地址作为所连接的容器的默认网关地址</strong>!</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">docker0 网桥是在 docker daemon 启动时自动创建的,其默认 IP 为 172.17.0.1/16,之后通过 bridge 驱动创建的容器都会在 docker0 的子网范围内选取一个未占用的 IP 使用,并连接到 docker0 网桥上。Docker daemon 提供了如下参数可以帮助用户自定义 docker0 的设置。</span></p>
<ul>
<li><span style="font-family: Microsoft YaHei; font-size: 15px">--bip=CIDR:设置 docker0 的 IP 地址和子网范围,使用 CIDR 格式,如 192.168.1.0/24。这个参数仅仅是配置 docker0 的,对用户自定义的网桥无效。</span></li>
<li><span style="font-family: Microsoft YaHei; font-size: 15px">--fixed-cidr=CIDR:限制 docker 容器可以获取的 IP 地址范围。Docker 容器默认可以获取的 IP 范围为 docker 网桥的整个子网范围,此参数可以将其缩小到某个子网范围内,所以这个参数必须在 docker 网桥的子网范围内。</span></li>
<li><span style="font-family: Microsoft YaHei; font-size: 15px">--mtu=BYTES:指定 docker0 网桥的最大传输单元(MTU)。</span></li>


</ul>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">除了使用 docker0 网桥外,用户还可以使用自定义的网桥,然后通过 --bridge=BRIDGE 参数传递给 docker daemon。比如我们可以创建一个自定义网桥 br0:</span></p>
<div class="cnblogs_code">
<pre>$ <span style="color: rgba(0, 0, 255, 1)">sudo</span><span style="color: rgba(0, 0, 0, 1)"> ip link add name br0 type bridge
$ </span><span style="color: rgba(0, 0, 255, 1)">sudo</span> <span style="color: rgba(0, 0, 255, 1)">ifconfig</span> br0 <span style="color: rgba(128, 0, 128, 1)">188.18</span>.<span style="color: rgba(128, 0, 128, 1)">0.1</span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623150915391-432625320.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">然后在启动 docker daemon 时设置参数 --bridge=br0 即可。</span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">iptables 规则</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">在安装 docker 时,会默认在宿主机中添加一些 iptables 规则,用于 docker 容器之间已经容器与外界的通信。我们可以通过 iptables-save 命令查看到 nat 表上 POSTROUTING 链上的有这么一条规则:</span></p>
<div class="cnblogs_code">
<pre>-A POSTROUTING -s <span style="color: rgba(128, 0, 128, 1)">172.17</span>.<span style="color: rgba(128, 0, 128, 1)">0.0</span>/<span style="color: rgba(128, 0, 128, 1)">16</span> ! -o docker0 -j MASQUERADE</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">这条规则关系着 docker 容器与外界的通信,其含义是将不是从网卡 docker0 发出的且源地址为 172.17.0.0/16 的数据包(容器中发出的数据包)做 SNAT。这样一来,从 docker 容器中访问外网的流量,在外部看来就是从宿主机上发出的,外部感觉不到 docker 容器的存在。</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">当外界想要访问 docker 容器运行的服务时该怎么办呢?接下来我们将启动一个简单的 web 服务器:</span></p>
<div class="cnblogs_code">
<pre>$ docker run -d -p <span style="color: rgba(128, 0, 128, 1)">3000</span>:<span style="color: rgba(128, 0, 128, 1)">3000</span> --name=myweb ljfpower/nodedemo</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">然后观察 iptables 规则的变化:</span></p>
<div class="cnblogs_code">
<pre>$ <span style="color: rgba(0, 0, 255, 1)">sudo</span> iptables-<span style="color: rgba(0, 0, 0, 1)">save

</span>*<span style="color: rgba(0, 0, 0, 1)">nat
</span>-A DOCKER ! -i docker0 -p tcp -m tcp --dport <span style="color: rgba(128, 0, 128, 1)">3000</span> -j DNAT --to-destination <span style="color: rgba(128, 0, 128, 1)">172.17</span>.<span style="color: rgba(128, 0, 128, 1)">0.3</span>:<span style="color: rgba(128, 0, 128, 1)">3000</span><span style="color: rgba(0, 0, 0, 1)">

</span>*<span style="color: rgba(0, 0, 0, 1)">filter
</span>-A DOCKER -d <span style="color: rgba(128, 0, 128, 1)">172.17</span>.<span style="color: rgba(128, 0, 128, 1)">0.3</span>/<span style="color: rgba(128, 0, 128, 1)">32</span> ! -i docker0 -o docker0 -p tcp -m tcp --dport <span style="color: rgba(128, 0, 128, 1)">3000</span> -<span style="color: rgba(0, 0, 0, 1)">j ACCEPT
…</span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">可以看到,在 nat 表和 filter 表中的 DOCKER 链中分别增加来一条规则,这两条规则将访问宿主机 3000 端口的请求转发到 172.17.0.3 的 3000 端口上(提供服务的 docker 容器的 IP 和端口),所以外界访问 docker 容器是通过 iptables 做 DNAT 实现的。</span><br><span style="font-family: Microsoft YaHei; font-size: 15px">Docker 默认的 forward 规则允许所有的外部 IP 访问容器,我们可以通过在 filter 的 DOCKER 链上添加规则来对外部的 IP 访问做出限制,比如只允许源 IP 为 192.168.21.212(笔者是在局域网内演示的)的数据包访问容器,添加的规则如下:</span></p>
<div class="cnblogs_code">
<pre>$ <span style="color: rgba(0, 0, 255, 1)">sudo</span> iptables -I DOCKER -i docker0 ! -s <span style="color: rgba(128, 0, 128, 1)">192.168</span>.<span style="color: rgba(128, 0, 128, 1)">21.212</span> -j DROP</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">不仅仅是与外界通信,docker 容器之间相互通信也受到 iptables 规则的限制。同一台宿主机上的 docker 容器默认都连在 docker0 网桥上,它们属于同一个子网,这是满足通信的第一步。同时,docker daemon 会在 filter 表的 FORWARD 链中增加一条&nbsp; ACCEPT 的规则(--icc=true):</span></p>
<div class="cnblogs_code">
<pre>-A FORWARD -i docker0 -o docker0 -j ACCEPT</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">这是满足容器间相互通信的第二步。当 docker daemon 的启动参数 -icc(icc 参数表示是否允许容器间相互通信) 设置为 false 时,上面的规则被设置为 DROP,容器间的相互通信就被禁止了,这时如果想让两个容器通信就需要在 docker run 命令中使用 --link 选项。</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">在 docker 容器和外界的通信过程中,还涉及了数据包在多个网卡之间的转发(比如从 docker0 网卡到宿主机 eth0 网卡),这需要内核将 ip forward 功能打开,就是把内核参数 ip_forward 设置为 1。Docker daemon 在启动的时候会执行这个操作,我们可以通过下面的命令进行检查:</span></p>
<div class="cnblogs_code">
<pre>$ <span style="color: rgba(0, 0, 255, 1)">cat</span> /proc/sys/net/ipv4/ip_forward</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623151222085-265850165.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">返回的结果为 1,表示内核的 ip forward 功能已经打开。</span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">容器的 DNS 和主机名(hostname)</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">使用同一个 docker 镜像可以创建很多个 docker 容器,但是这些容器的 hostname 并不相同,也就是说 hostname 并没有被写入到镜像中。实际上容器中的 /etc 目录下有 3 个文件是在容器启动后被虚拟文件覆盖掉的,分别是 /etc/hostname、/etc/hosts 和 /etc/resolv.conf,通过在容器中运行 mount 命令可以看到它们:</span></p>
<div class="cnblogs_code">
<pre># <span style="color: rgba(0, 0, 255, 1)">mount</span><span style="color: rgba(0, 0, 0, 1)">

</span>/dev/mapper/ubuntu--vg-root on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=<span style="color: rgba(0, 0, 0, 1)">ordered)
</span>/dev/mapper/ubuntu--vg-root on /etc/<span style="color: rgba(0, 0, 255, 1)">hostname</span> type ext4 (rw,relatime,errors=remount-ro,data=<span style="color: rgba(0, 0, 0, 1)">ordered)
</span>/dev/mapper/ubuntu--vg-root on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=<span style="color: rgba(0, 0, 0, 1)">ordered)
…</span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">这种方式既能解决主机名的问题,同时也能让 DNS 及时更新(改变 resolv.conf)。由于这些文件的维护方法会随着 docker 版本的升级而不断变化,所以尽量不要修改这些文件,而是通过 docker 提供的相关参数进行设置,其参数配置方式如下。</span></p>
<ul>
<li><span style="font-family: Microsoft YaHei; font-size: 15px">-h HOSTNAME 或者 --hostname=HOSTNAME:设置容器的 hostname,此名称会写入到 /etc/hostname 和 /etc/hosts 文件中,也可以在容器的 bash 提示符中看到。</span></li>
<li><span style="font-family: Microsoft YaHei; font-size: 15px">--dns=IP_ADDRESS…:为容器配置 DNS,会被写入到 /etc/resolv.conf 文件中。</span></li>
</ul>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">这两个参数都是针对容器的需要在创建容器时进行设置。比如下面的 demo:</span></p>
<div class="cnblogs_code">
<pre>$ docker run -it --name mycon -h lion --dns=<span style="color: rgba(128, 0, 128, 1)">8.8</span>.<span style="color: rgba(128, 0, 128, 1)">8.8</span> ubuntu:<span style="color: rgba(128, 0, 128, 1)">14.04</span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="https://images2018.cnblogs.com/blog/952033/201806/952033-20180623151338031-1242140309.png"></span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">总结</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">本文主要通过演示 docker0 网桥相关的功能来探索 docker 网络中的 bridge 驱动的实现机制。从本文中不难看出,linux 系统中,docker 的 bridge 驱动是依赖于系统的 ip forward 以及 iptables 等核心功能的。因此在学习 docker 的过程中,适当的补充 linux 相关的知识也是十分必要的!</span></p>
<p><strong><span style="font-family: Microsoft YaHei; font-size: 15px">参考:</span></strong><br><span style="font-family: Microsoft YaHei; font-size: 15px">《docker 容器与容器云》</span></p>

</div>
<div id="MySignature" role="contentinfo">
    <div>作者:sparkdev</div>
<div>出处:http://www.cnblogs.com/sparkdev/</div>
<div>本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。</div><br><br>
来源:https://www.cnblogs.com/sparkdev/p/9217310.html
頁: [1]
查看完整版本: Docker 网络之理解 bridge 驱动