Docker安装时会自动在 host 上创建三个网络,none、host、bridge我们可用docker network ls命令查看。

一、Docker网络架构

Docker在1.9版本中引入了一整套的docker network子命令和跨主机网络支持。这允许用户可以根据他们应用的拓扑结构创建虚拟网络并将容器接人其所对应的网络。其实,早在Docker1.7版本中,网络部分代码就已经被抽离并单独成为了Docker的网络库,即libnetwork。在此之后,容器的网络模式也被抽象变成了统一接口的驱动。

为了标准化网络驱动的开发步骤和支持多种网络驱动,Docker公司在libnetwork中使用了CNM ( Container Network Model )。CNM定义了构建容器虚拟化网络的模型,同时还提供了可以用于开发多种网络驱动的标准化接口和组件。

libnetwork和Docker daemon和各个网络驱动关系:

image-20211010223218172

Docker daemon通过调用libnetwork对外提供的API完成网络的创建和管理等功能。libnetwork中则使用了CNM来完成网络功能的提供。而CNM中主要有沙盒( sandbox )、端点( endpoint)和网络( network)这3种组件。libnetwork中内置的5种驱动则为libnetwork提供了不同类型的网络服务。下面分别对CNM中的3个核心组件和libnetwork中的5种内置驱动进行介绍。

CNM的核心组件

  • 沙盒:一个沙盒包含了一个容器网络栈的信息。沙盒可以对容器的接口、路由和DNS设置等进行管理。沙盒的实现可以是Linux network namespace 、FreeBSD Jail或者类似的机制。一个沙盒可以有多个端点和多个网络。
  • 端点:一个端点可以加入一个沙盒和一个网络。端点的实现可以是veth pair , Open vSwitch内部端口或者相似的设备。一个端点只可以属于一个网络并且只属于一个沙盒。
  • 网络:一个网络是一组可以直接互相联通的端点。网络的实现可以是Linux bridge、VLAN。一个网络包含多个端点。

5种内置驱动

  • bridge驱动。Docker的默认设置,这个驱动,libnetwork将创建出来的Docker容器连接到Docker网桥上作为最常规的模式, bridge模式已经可以满足Docker容器最基本的使用需求了。然而其与外界通信使用NAT,增加了通信的复杂性,在复杂场景下使用会有诸多限制。
  • host驱动。libnetwork将不为Docker容器创建网络协议栈,即不会创建独立的network namespace。Docker容器中的进程处于宿主机的网络环境中,相当于Docker容器和宿主机共用同一个network namespace,使用宿主机的网卡、IP和端口等信息。但是,容器其他方面还是和宿主机隔离的。host模式很好地解决了容器与外界通信的地址转换问题,可以直接使用宿主机的IP进行通信,不存在虚拟化网络带来的额外性能负担。但是host驱动也降低了容器与容器之间、容器与宿主机之间网络层面的隔离性,引起网络资源的竞争与冲突。因此可以认为host驱动适用于对于容器集群规模不大的场景。
  • overlay驱动。此驱动采用IETF标准的VXLAN方式,并且是VXLAN中被普遍认为最适合大规模的云计算虚拟化环境的SDN controller模式。在使用的过程中,需要一个额外的配置存储服务,例如Consul、etcd或ZooKeeper。还需要在启动Docker daemon的的时候额外添加参数来指定所使用的配置存储服务地址。
  • remote驱动。这个驱动实际上并未做真正的网络服务实现,而是调用了用户自行实现的网络驱动插件,使libnetwork实现了驱动的可插件化,更好地满足了用户的多种需求。用户只要根据libnetwork提供的协议标准,实现其所要求的各个接口并向Docker daemon进行注册。
  • null驱动。使用这种驱动的时候,Docker容器拥有自己的network namespace,但是并不为容器进行任何网络配置。也就是说,这个容器除了network namespace自带的loopback网卡外,没有其他任何网卡、IP、路由等信息,需要用户为Docker容器添加网卡、配置IP等。这种模式如果不进行特定的配置是无法正常使用的,但是优点也非常明显,它给了用户最大的自由度来自定义容器的网络环境。

二、bridge驱动

docker0网桥

当Host在安装完docker之后,宿主机上通过ifconfig就会发现有一块docker0的网卡,宿主机上也会在内核路由表上添加一条到达相应网络的静态路由。

使用docker创建出的容器,可以看到它有2块网卡lo和eth0。lo的回环网卡,eth0是容器与外界通信的网卡,他的IP与宿主机上网桥docker0在同一个网段。

在控制台查看宿主机时,会发现有一块veth网卡,例如veth043f86,而veth pair成对出现,用来连接2个network namespace,所以另外一个就是eth0。容器的eth0与宿主机docker0相连,而veth也是与宿主机相连,docker0就是一个网桥

image-20211010225439264

Docker bridge模式创建了docker0网桥,并以veth pair连接容器网络,容器中数据通过docker0网桥转发到eth0网卡中。

这里网桥的概念等同于交换机,为连在其上的设备转发数据帧。网桥上的veth网卡设备相当于交换机上的端口,可以将多个容器或虚拟机连接在其上,这些端口工作在二层,所以是不需要配置IP信息的。docker0网桥就为连在其上的容器转发数据帧,使得同一台宿主机上的Docker容器之间可以相互通信。读者应该注意到docker0既然是二层设备,其上怎么也配置了IP呢?docker0是普通的Linux网桥,它是可以在上面配置IP的,可以认为其内部有一个可以用于配置IP信息的网卡接口。在Docker的桥接网络模式中,docker0的IP地址作为连于之上的容器的默认网关地址存在。
在Linux中,可以使用brctl命令查看和管理网桥(需要安装bridge-utils软件包)如查看本机上的Linux网桥以及其上的端口:

1
$  brctl show

dockero网桥是在Docker daemon启动时自动创建的,其IP默认为172.17.0.1/16,之后创建的Docker容器都会在dockero子网的范围内选取一个未占用的IP使用,并连接到dockero网桥上。

iptables规则

Docker安装完成后,将默认在宿主机系统上增加一些iptables规则,以用于Docker容器和容器之间以及和外界的通信,可以使用iptables-save命令查看。其中nat表上的POSTROUTING链有这么一条规则:

1
-APOSTROUTING -s 172.17.0.0/16 ! -o dockero -j MASQUERADE

这条规则关系着Docker容器和外界的通信,含义是将源地址为172.17.0.0/16的数据包(即Docker容器发出的数据),当不是从dockero网卡发出时做SNAT(源地址转换,将IP包的源地址替换为相应网卡的地址)。这样一来,从Docker容器访问外网的流量,在外部看来就是从宿主机上发出的,外部感觉不到Docker容器的存在。

Docker的forward规则默认允许所有的外部IP访问容器,可以通过在filter的DOCKER链上添加规则来对外部的IP访问做出限制,如只允许源IP为8.8.8.8的数据包访问容器,需要添加如下规则:

1
iptables -I DOCKER -i docker0 ! -s 8.8.8.8 -j DROP

Docker容器之间互相通信也受到iptables规则现在。同一宿主机上Docker容器默认连在docker0网桥上,同属一个子网,同时Docker daemon会在filter的FORWARD链中添加一条ACCEPT规则(–icc=true):

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

当Docker daemon启动参数–icc(是否允许容器互相通信)设置为false,规则就会设置为DROP,通信被禁止,想让2个容器通信就需要在docker run时使用–link选项。

在Docker容器和外界通信的过程中,还涉及了数据包在多个网卡间的转发(如从dockero网卡到宿主机etho的转发)这需要内核将ip-forward功能打开,即将ip_forward系统参数设为1。Dockerdaemon启动的时候默认会将其设为1 ( –ip-forward=true ),也可以通过以下命令手动设置:

1
2
3
$  echo 1 > / proc/sys/net/ipv4/ip_forward
$ cat /proclsys/net/ipv4/ip_forward
1

Docker daemon启动参数:

  • --iptables:是否允许Docker daemon设置宿主机的iptables规则,默认为true。
  • --icc:是否允许Docker容器间相互通信,默认为true。true或false改变的是FORWARD链中相应iptables规则的策略(ACCEPT、DROP)。由于操作的是iptables规则,所以需要--iptables=true才能生效。
    --ip-forward:是否将ip_forward参数设为1,默认为true,用于打开Linux内核的ip数据包转发功能

Docker容器的DNS和主机名

同一个Docker镜像可以启动很多个Docker容器,它们的主机名并不一样,也即是说主机名并非是被写入镜像中的。实际上容器中/etc目录下有3个文件是容器启动后被虚拟文件覆盖掉的,分别是/etc/hostname/etc/hosts/etc/resolv.conf,通过在容器中运行mount命令可以查看。

1
2
3
4
5
$$ mount
/devldisk/by-uuid/1fec...ebdf on /etc/hostname type ext4...
/dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ...
/dev/disk/by-uuid/1fec...ebdf on /etc/resolv.conf type ext4 ...

这样能解决主机名的问题,同时也能让DNS及时更新(改变resolv.conf )。通过Docker提供的参数进行相关设置,参数配置方式如下。

  • -h HOSTNAME 或者--hostname=HOSTNAME:设置容器的主机名,此名称会写在/etc/hostname/etc/hosts文件中,也会在容器的bash提示符中看到。但是在外部,容器的主机名是无法查看的,不会出现在其他容器的hosts文件中,即使使用docker ps命令也查看不到。此参数是docker run命令的参数,而非Docker daemon的启动参数。
  • --dns=IP_ADDRESS...:为容器配置DNS,写在/etc/resolv.conf中。该参数既可以在Dockerdaemon启动的时候设置也可以在docker run时设置,默认为8.8.8.8和8.8.4.4。

这3个文件不会被docker commit保存,重启容器会导致修改失效。

三、Docker网络配置原理

Docker1.6之前,Docker网络分为2个部分:Docker daemon的网络配置和libcontainer网络配置。Docker daemon的网络指daemon启动时,在主机系统上所做的网络设置,可以被所有Docker容器所使用;libcontainer的网络配置则针对具体的容器,是在使用docker run命令启动容器时,根据传入的参数为容器做的网络配置工作。

Docker1.7之后将所有网络相关代码抽离,整合为单独的库libnetwork,通过插件形式允许用户自己根据自己需求实现自己的network driver。

Docker daemon网络配置原理

1.网络相关的参数

Docker daemon启动时,都会初始化自己的网络环境,为Docker容器提供网络通信服务。所有网络相关参数配置在bridgeConfig中:

  • EnableIptables:默认值为true,对应于Docker daemon启动时的–iptables参数,作用为是否允许Docker daemon在宿主机上添加iptables规则
  • EnableIpMasq:默认为true,对应于Docker daemon启动时的–ip-masq参数,作用为是否为Docker容器通往外界的包做SNAT,此变量即控制是否添加那条规则。
  • DefaultIp:对应–ip参数,默认值为“0.0.0.0”。这个变量的作用为:当启动容器做端口映射时,将DefaultIp作为默认使用的IP地址
  • EnableIpForward 、 Iface ,IP 、 FixedCIDR, InterContainerCommunication分别对应–ip-forward、–bridge、–bip、 –fixed-cidr、–icc。

2.初始化过程

网络参数校验

解析参数,并赋予相应的变量。检查互斥配置。

是否初始化bridge驱动

参数校验完成后,接着判断Iface和disableNetworkBridge的值是否相同,Iface保存的是网桥名称,disableNetworkBridge是一个字符串常量,值为none。因此,若用户通过传过来的参数将Iface设为none,则config.DisableBridge变量为true,否则为false。

接下来会调用libnetwork.New()生成网络控制器controller,这个控制器主要用于创建和管理Network。然后会通过null驱动和host驱动来进行默认的网络创建。

最后会根据DisableBridge的值来决定bridge驱动是否进行初始化。若DisableNetwork为false,则运行initBridgeDriver函数。initBridgeDriver函数就是完成默认的bridge驱动的初始化任务。

处理网桥参数

已经知道Docker网桥默认为docker0,也可以通过–bridge参数指定自定义的网桥。处理用户自定义网桥的流程分为如下两步。

(1)将用户指定的网桥名称传入Iface,若Iface不为空,则将其传赋值给bridgeName。如果Iface为空,则将bridgeName指定为DefaultNetworkBridge。DefaultNetworkBridge是一个字符串常量,为docker0,即表示当用户没有传入网桥参数时,启用默认网桥docker0。

(2)首先,寻找Docker网桥名是否在宿主机上有对应的显卡,如果存在则返回其IP等信息,否则则从系统预定义的IP列表中分配一个可用IP。如果用户没有使用–bip来指定Docker网桥的IP地址,那么上面得到的IP会被写入ipamV4Conf结构体中,此结构体用于保存关于Docker网桥上有关IPV4的相关信息,如果用户进行了指定则会将指定的IP信息写入ipamV4Conf结构体中。接下来,如果FixedCIDR参数不为空,则将用户传入的网络范围写入到ipamV4Conf结构体中。如果默认的网关不为空,则将其信息写人到ipamV4Conf结构体中。然后,如果FixedCIDRv6,则将用户指定的IPV6网络范围和相关的IPV6配置信息写入ipamV6Conf中。最后使用上述信息作为参数调用controller.NewNetwork()函数,并指定bridge驱动来创建Docker网桥。

创建网桥设置队列

当需要Docker daemon创建网络时,则调用controller.NewNetwork()函数来通过libnetwork完成创建,实现过程的主要步骤:

  1. 使用IP管理器的默认驱动创建IP管理器,并使用IP管理器从其自身维护的IP池中获取参数中指定的IP地址段
  2. 在确保新的网络设置和已经存在的网络不冲突之后,创建与这个驱动(即bridge驱动)相符的配置结构体network。接下来根据配置中的网桥名寻找对应的网桥。如果网桥不存在,则将创建网桥的步骤加入设置队列。
  3. 定义关于网络隔离的iptables规则设置的函数,在接下来的步骤中加入到设置队列中,以确保不同网络之间相互隔离。
  4. 将IPV4配置到网桥上、IPV6配置、IPV6转发、开启本地回环接口的地址路由、开启iptables、IPV4和IPV6的网关信息配置、网络隔离的iptables规则设置和网桥网络过滤等步骤加入到设置队列中。
  5. 最后,运行设置队列中的所有步骤,主要通过netlink进行系统调用来完成Docker网桥的创建和配置工作。
更新相关配置信息

完成上述操作后,libnetwork会将各种相关配置信息存储到Docker的LibKv数据仓库中,以备后续的查找和使用。

libcontainer网络配置原理

Docker容器的网络就是在创建特定容器的时候,根据传入的参数为容器配置特定的网络环境,主要内容包括为容器配置网卡、IP、路由、DNS等一系列任务。Docker容器一般使用docker run命令来创建,其关于网络方面的参数有--net--dns等。–net是一个非常重要的参数,用于指定容器的网络模式。

1.命令行参数阶段

docker run命令执行的时候,首先会创建一个DockerCli类型的变量来表示Docker客户端,然后根据具体命令调用相应函数完成请求,如run命令就是调用CmdRun函数完成的。实现功能有:

  • 解析docker run命令的参数,并存入相应的变量( config、hostConfig、networkingConfig、cmd等)中。
  • 发送请求给Docker daemon,创建Docker容器对象,完成容器启动前的准备工作。
  • 发送请求给Docker daemon,启动容器。

Docker run命令中提供的关于容器配置的参数首先保存在了ConfigHostConfig以及NetworkingConfig这3个结构中。结构体定义都放在在engine-api项目中的types包中,Config保存的是不依赖于宿主机的信息,也就是可以迁移的信息,其他与宿主机关联的信息都保存在HostConfig中,在Docker将网络模块独立为一个项目后,将网络参数部分从原来的配置中抽出为NetworkingConfig。Config中保存有Hostname(容器主机名)、NetworkDisabled(是否关闭容器网络功能)、MacAddress (网卡MAC地址)等;HostConfig保存有Dns(容器的DNS)、NetworkMode(容器的网络模式);NetworkingConfig保存了一组端点参数与所属网络名的map,docker run与docker network connect的网络配置均会保存在该map中。以上配置项都有对应的命令行参数。

解析完docker run命令行参数以后,Docker客户端利用Docker daemon暴露的API接口,分别将创建容器与启动容器的请求发送至Docker daemon,完成容器的创建和启动。因此CmdRun函数除了解析并组装与网络相关的命令行参数外,不做网络方面的具体配置,具体的网络配置还是由Docker daemon来完成。

2.创建容器阶段

当Docker客户端将创建容器的请求发送给Docker daemon后,Docker daemon开始创建容器,主要完成以下工作。

  • 校验hostConfig、Config与NetworkingConfig中的参数。
  • 根据需要调整HostConfig的参数。
  • 根据传入的容器配置和名称创建对应的容器。

容器创建的最终返回一个Container对象,Container对象就是容器的数据结构表示,其中有-个名为NetworkSettings的属性,描述了容器的具体网络信息,其结构主要为:

属性 作用
Bridge 容器所连接到的网桥
SandboxID 容器对应Sandbox的ID
HairpinMode 是否开启hairpin模式
Ports 容器映射的端口号
SandboxKey Sandbox对应network namespace文件的路径
Networks 保存了容器端点配置与所属网络名的map
IsAnonymousEndpoint 容器是否未指定名字name

除了IsAnonymousEndpoint,都是在启动阶段才能完全确定。

3.启动容器阶段

容器创建完成之后, Docker客户端会发送启动容器请求。daemon首先获取到需要启动的容器,然后调用容器的Start函数去真正启动容器,其中与网络相关的主要:

  • initializeNetwork:初始化Container对象中与网络相关的属性;
  • populateCommand:填充Docker Container内部需要执行的命令,Command中含有进程启动命令,还含有容器环境的配置信息,也包括网络配置;
  • container.waitForStart:实现Docker Container内部进程的启动,进程启动之后,为进程创建网络环境等。

4.execdriver网络执行流程

execdriver是Docker daemon的执行驱动,用来启动容器内部进程的执行。
这里主要是配置表示命名空间的namespaces属性,namespaces列出了当启动容器进程时需要新创建的命名空间。network namespace的配置是通过调用execdrivercreateNetwork函数实现的。该函数根据Docker容器的不同网络模式执行不同的动作,流程如下。

  1. 根据execdriver.Command对象中的Network属性判断出采用不同的方式配置网络。
  2. 若Network.ContainerID不为空,则为container模式,则首先在处于活动状态的容器列表中查找被引用的容器,接着找到被引用容器中进程的network namespace路径。假如被引用容器的第一个进程在主机中的PID为12345,则network namespace的路径为/proc/12345/ns/net。然后将该路径放入到libcontainer.Config.Namespaces中。
  3. 若Network.NamespacePath不为空,对应host模式,则将Network.NamespacePath写入libcontainer.Config.Namespaces中。
  4. 其他情况下,表示目前暂时无法获得network namespace,则为libcontianer设置PreStart钩子函数,主要工作是遍历execdriver提供的preStart钩子函数并执行。前面daemon中调用Run函数时已经将setNetworkNamespaceKey函数封装为PreStart钩子函数了。

createNetwork函数执行完后,就已经把network namespace信息或者能够配置network
namespace的钩子函数全部记录到libcontainer里了。然后容器就开始执行,所以接着进入libcontainer中继续跟踪容器的网络。

5.libcontainer网络执行流程

在libnetwork被分离出来前,Docker网络的内核态配置是由libcontianer完成的,但在Docker容器启动的调用流程下,libcontainer只是负责触发libcontianer.Config .Hooks中的Prestart钩子函数来完成容器网络的底层配置,具体触发的地方位于libcontainer/process_linux.go文件中的initProcess.start方法中,在容器的init进程启动时调用。虽然随后的createNetworkInterfaces函数仍然存在并被调用了,但由于该函数是通过遍历libcontainer.Config.Networks数组内定义好的网络信息来配置网络的,而前面execdriver并未填充该数组,所以Docker容器启动流程下并不会在libcontainer中创建网络环境。暂时保留这部分只是为了兼容一些遗留代码。

这里讲解一下容器启动前触发的Prestart钩子函数setNetworkNamespaceKey,虽然该函数真正定义的地方是在daemon/container_operations_unix.gosetNetworkNamespaceKey的主要工作是获取network namespace并与容器对应的sandbox关联起来。首先通过容器pid获取容器networknamespace文件的位置—/proc/[pid]/ns/net,再通过容器ID获取其对应的sandbox,最后调用sandbox的SetKey完成底层网络的创建

6.libnetwork实现内核态网络配置

libnetwork对内核态网络的配置包括启动容器libcontainer网络执行流程两个阶段。