笔者在前文《Docker 网络之进阶篇》中介绍了 CNM(Container Network Model),并演示了 bridge 驱动下的 CNM 使用方式。为了深入理解 CNM 及最常用的 bridge 驱动,本文将探索 bridge 驱动的实现机制。
说明:本文的演示环境为 ubuntu 16.04。

dokcer0 网桥

在 Ubuntn 上安装 docker 后,宿主机上默认被创建了一个名为 docker0 的网卡,其 IP 为 172.17.0.1/16:

有了这个网卡,宿主机还会在内核的路由表中添加一条到达相应网络的静态路由记录:

这条路由信息表示所有目的 IP 为 172.17.0.0/16 的数据包都会从 docker0 网卡发出。接下来我们创建一个名为 mycon 的容器,并观察其网络配置:

在 mycon 容器内可以看到两块网卡:lo 和 eth0。其中 lo 是容器的回环网卡,eth0 是容器与外界通信的网卡,eth0 的 IP 信息为 172.17.0.2/16,和宿主机上的网卡 bridge0 在同一网段中。查看 mycon 的路由信息:

mycon 容器的默认网关正是宿主机的 docker0 网卡。通过 ping 命令测试与外网的连通性,此时容器 mycon 是可以连通外网的,这就说明 mycon 的 eth0 网卡与宿主机的 docker0 网卡是连通的。

下面我们来查看宿主机的网络设备:

发现多了一个以 "veth" 开头的网卡,这是一个 veth 设备。而 veth 设备总是成对出现的,那么与 veth7537a16 配对的就应该是 mycon 容器中的 eth0 了。既然 mycon 容器中的 eth0 是与 docker0 连通的,那么 veth7537a16 也应该是与 docker0 连通的。因此 docker0 并不是一个简单的网卡设备,而是一个网桥!下图展示了 docker bridge 网络模式的拓扑图:

事实上,docker 创建了 docker0 网桥,并以 veth pair 连接各个容器的网络,容器中的数据通过 eth0 发送到 docker0 网桥上,并由 docker0 网桥完成转发。这里网桥的概念等同于交换机,为连在其上的设备转发数据帧。网桥上的 veth 网卡设备相当于交换机上的端口,可以将多个容器连接在它们上面,这些端口工作在二层,所以是不需要配置 IP 信息的。上图中的 docker0 网桥就为连在其上的容器转发数据帧,使得同一台宿主机上的 docker 容器之间可以相互通信。既然 docker0 是二层设备,那么它为什么还需要 IP 呢?其实,docker0 是一个普通的 linux 网桥,是可以为它配置 IP 的,我们可以认为它的内部有一个可以用于配置 IP 的网卡。Docker0 的 IP 地址作为所连接的容器的默认网关地址

docker0 网桥是在 docker daemon 启动时自动创建的,其默认 IP 为 172.17.0.1/16,之后通过 bridge 驱动创建的容器都会在 docker0 的子网范围内选取一个未占用的 IP 使用,并连接到 docker0 网桥上。Docker daemon 提供了如下参数可以帮助用户自定义 docker0 的设置。

  • --bip=CIDR:设置 docker0 的 IP 地址和子网范围,使用 CIDR 格式,如 192.168.1.0/24。这个参数仅仅是配置 docker0 的,对用户自定义的网桥无效。
  • --fixed-cidr=CIDR:限制 docker 容器可以获取的 IP 地址范围。Docker 容器默认可以获取的 IP 范围为 docker 网桥的整个子网范围,此参数可以将其缩小到某个子网范围内,所以这个参数必须在 docker 网桥的子网范围内。
  • --mtu=BYTES:指定 docker0 网桥的最大传输单元(MTU)。

除了使用 docker0 网桥外,用户还可以使用自定义的网桥,然后通过 --bridge=BRIDGE 参数传递给 docker daemon。比如我们可以创建一个自定义网桥 br0:

$ sudo ip link add name br0 type bridge
$ sudo ifconfig br0 188.18.0.1

然后在启动 docker daemon 时设置参数 --bridge=br0 即可。

iptables 规则

在安装 docker 时,会默认在宿主机中添加一些 iptables 规则,用于 docker 容器之间已经容器与外界的通信。我们可以通过 iptables-save 命令查看到 nat 表上 POSTROUTING 链上的有这么一条规则:

-A POSTROUTING -s 172.17.0.0/ ! -o docker0 -j MASQUERADE

这条规则关系着 docker 容器与外界的通信,其含义是将不是从网卡 docker0 发出的且源地址为 172.17.0.0/16 的数据包(容器中发出的数据包)做 SNAT。这样一来,从 docker 容器中访问外网的流量,在外部看来就是从宿主机上发出的,外部感觉不到 docker 容器的存在。

当外界想要访问 docker 容器运行的服务时该怎么办呢?接下来我们将启动一个简单的 web 服务器:

$ docker run -d -p : --name=myweb ljfpower/nodedemo

然后观察 iptables 规则的变化:

$ sudo iptables-save

*nat
-A DOCKER ! -i docker0 -p tcp -m tcp --dport -j DNAT --to-destination 172.17.0.3:

*filter
-A DOCKER -d 172.17.0.3/ ! -i docker0 -o docker0 -p tcp -m tcp --dport -j ACCEPT

可以看到,在 nat 表和 filter 表中的 DOCKER 链中分别增加来一条规则,这两条规则将访问宿主机 3000 端口的请求转发到 172.17.0.3 的 3000 端口上(提供服务的 docker 容器的 IP 和端口),所以外界访问 docker 容器是通过 iptables 做 DNAT 实现的。
Docker 默认的 forward 规则允许所有的外部 IP 访问容器,我们可以通过在 filter 的 DOCKER 链上添加规则来对外部的 IP 访问做出限制,比如只允许源 IP 为 192.168.21.212(笔者是在局域网内演示的)的数据包访问容器,添加的规则如下:

$ sudo iptables -I DOCKER -i docker0 ! -s 192.168.21.212 -j DROP

不仅仅是与外界通信,docker 容器之间相互通信也受到 iptables 规则的限制。同一台宿主机上的 docker 容器默认都连在 docker0 网桥上,它们属于同一个子网,这是满足通信的第一步。同时,docker daemon 会在 filter 表的 FORWARD 链中增加一条  ACCEPT 的规则(--icc=true):

-A FORWARD -i docker0 -o docker0 -j ACCEPT

这是满足容器间相互通信的第二步。当 docker daemon 的启动参数 -icc(icc 参数表示是否允许容器间相互通信) 设置为 false 时,上面的规则被设置为 DROP,容器间的相互通信就被禁止了,这时如果想让两个容器通信就需要在 docker run 命令中使用 --link 选项。

在 docker 容器和外界的通信过程中,还涉及了数据包在多个网卡之间的转发(比如从 docker0 网卡到宿主机 eth0 网卡),这需要内核将 ip forward 功能打开,就是把内核参数 ip_forward 设置为 1。Docker daemon 在启动的时候会执行这个操作,我们可以通过下面的命令进行检查:

$ cat /proc/sys/net/ipv4/ip_forward

返回的结果为 1,表示内核的 ip forward 功能已经打开。

容器的 DNS 和主机名(hostname)

使用同一个 docker 镜像可以创建很多个 docker 容器,但是这些容器的 hostname 并不相同,也就是说 hostname 并没有被写入到镜像中。实际上容器中的 /etc 目录下有 3 个文件是在容器启动后被虚拟文件覆盖掉的,分别是 /etc/hostname、/etc/hosts 和 /etc/resolv.conf,通过在容器中运行 mount 命令可以看到它们:

# mount

/dev/mapper/ubuntu--vg-root on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/mapper/ubuntu--vg-root on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/mapper/ubuntu--vg-root on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered)

这种方式既能解决主机名的问题,同时也能让 DNS 及时更新(改变 resolv.conf)。由于这些文件的维护方法会随着 docker 版本的升级而不断变化,所以尽量不要修改这些文件,而是通过 docker 提供的相关参数进行设置,其参数配置方式如下。

  • -h HOSTNAME 或者 --hostname=HOSTNAME:设置容器的 hostname,此名称会写入到 /etc/hostname 和 /etc/hosts 文件中,也可以在容器的 bash 提示符中看到。
  • --dns=IP_ADDRESS…:为容器配置 DNS,会被写入到 /etc/resolv.conf 文件中。

这两个参数都是针对容器的需要在创建容器时进行设置。比如下面的 demo:

$ docker run -it --name mycon -h lion --dns=8.8.8.8 ubuntu:14.04

总结

本文主要通过演示 docker0 网桥相关的功能来探索 docker 网络中的 bridge 驱动的实现机制。从本文中不难看出,linux 系统中,docker 的 bridge 驱动是依赖于系统的 ip forward 以及 iptables 等核心功能的。因此在学习 docker 的过程中,适当的补充 linux 相关的知识也是十分必要的!

参考:
《docker 容器与容器云》

Docker 网络之理解 bridge 驱动的更多相关文章

  1. Docker网络详细理解-容器网络互通

    这篇文章主要解决以下几个问题: 1.同一个网段的容器互相之间通过ip进行ping通 2.同一个网段的容器互相之间通过容器名,通过使用--link进行ping通,已放弃这种方法 3.同一个网段的容器互相 ...

  2. Docker 网络原理

    引言 学习docker网络,可以带着下面两个问题来探讨 容器之间可以相互访问的原理 容器暴露端口后,通过宿主机访问到容器内应用,并且对于访问端而言不用感知容器存在的原理 Docker 本身的技术依赖L ...

  3. 理解Docker(5):Docker 网络

    本系列文章将介绍 Docker的相关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...

  4. 理解Docker :Docker 网络

    本系列文章将介绍 Docker的相关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...

  5. bridge br0 docker 网络问题 Docker Container与Docker Host

    Docker学习笔记:Docker 网络配置 - docker ppt - docker中文社区http://www.docker.org.cn/dockerppt/111.html Bridge t ...

  6. docker网络之bridge

    建议阅读本文章之前了解一下文章,本文不作bridge的基本介绍 https://blog.csdn.net/u014027051/article/details/53908878/ http://wi ...

  7. Docker网络——单host网络

    前言 前面总结了Docker基础以及Docker存储相关知识,今天来总结一下Docker单主机网络的相关知识.毋庸置疑,网络绝对是任何系统的核心,他在Docker中也占有重要的作用.同样本文基于Clo ...

  8. Docker网络解决方案 - Calico部署记录

    简单来说,实现docker跨主机容器间通信,常用的第三方网络方案是Flannel,Weave,Calico:Flannel会为每个host分配一个subnet,容器从这个subnet中分配ip,这些i ...

  9. Docker网络解决方案 - Weave部署记录

    前面说到了Flannel的部署,今天这里说下Docker跨主机容器间网络通信的另一个工具Weave的使用.当容器分布在多个不同的主机上时,这些容器之间的相互通信变得复杂起来.容器在不同主机之间都使用的 ...

随机推荐

  1. Luogu 3384 【模板】树链剖分

    题目描述 如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z 操作2: 格式 ...

  2. 使用gulp打包普通项目

    前言: 在使用gulp打包工具之前,我做的H5项目在浏览器中的缓存是很严重的,若改了一点css,加了一句js代码,不手动清除浏览器缓存是看不到效果的.老总也在项目演示当中遇到这些问题,一查找原因却是缓 ...

  3. php位运算

    php位运算 /** * 位运算 */ echo "<pre>"; $aa = $a&$b; //按位与,相同位都为1时为1,其他都为0; echo " ...

  4. H5_ 表单及其他新增和改良元素

    1. form属性 formaction属性 formmethod属性 formenctype属性 formtarget属性 <!DOCTYPE html> <html lang=& ...

  5. 第一次冲刺意见汇总&团队第一阶段总结

    大家对我们小组的意见基本是: 1.设计界面简单 2.功能较少 3.没有实现切换歌曲的功能 谢谢HT小组的走心评价 接下来我们组内准备:1.先调节用户界面,插入一些图片,美化界面,给用户直观的体验上升. ...

  6. js 事件模型详解

    把js的事件模型,分为两类,DOM0级和DOM2级, DOM0级 通常直接在DOM对象上绑定函数对象,指定事件类型,dom.onClick = function(){};类似于这种写法,移除事件,则直 ...

  7. C语言复习2_运算符

    今天复习一下C语言的运算符 1.赋值运算符 单等号 = 顺序是:从右往左 2.复合运算符 #include <stdio.h> #include <stdlib.h> int ...

  8. 使用bind提供域名解析服务搭建

    正向解析实验 1.安装bind服务 2.在/etc目录中找到该服务程序的主配置文件,然后把第11行和第17行的地址均修改为any 3.正向解析参数如下: 4.编辑数据配置文件,从/var/named目 ...

  9. emWin表盘界面设计,含uCOS-III和FreeRTOS两个版本

    第4期:简易表盘界面设计 配套例子:V6-906_STemWin提高篇实验_简易表盘界面设计(uCOS-III)V6-907_STemWin提高篇实验_简易表盘界面设计(FreeRTOS) 例程下载地 ...

  10. 理解Golang哈希表Map的元素

    目录 概述 哈希函数 冲突解决 初始化 结构体 字面量 运行时 操作 访问 写入 扩容 删除 总结 在上一节中我们介绍了 数组和切片的实现原理,这一节会介绍 Golang 中的另一个集合元素 - 哈希 ...