Docker 网络学习笔记


简介

Docker在网络方面并没有特别出彩的地方。没有复杂的网络设置,如果需要支持复杂应用的网络需求,光靠Docker的设置参数也是不够的,还需要借助linux本身的一些网络设置工具。

本文只总结Docker本身的网络能力,对于更复杂的网络设置,不属于Docker网络详解这个题目的范畴了,文章还是要短小精悍比较好读,当然我也好写。

下面会按照以下思路来总结下Docker的网络:

1. Docker daemon启动时对网络干了什么?

2. Container默认的启动方法有哪些网络能力?

3. Container如何对外暴露网络服务?

4. Container之间的相互通信。

(注:本文基于Docker v1.0.1 代码分析)


网络初始化

我们先来看看,在不加任何参数的情况下,Docker daemon初始化对网络干了什么。运行  docker -d & 后,会有如下打印(省略了无关联部分): 

[html]  view plain copy print ?
  1. [/var/lib/docker|116d5cd4] +job init_networkdriver()  
  2. [/var/lib/docker|116d5cd4.init_networkdriver()] creating new bridge for docker0  
  3. [/var/lib/docker|116d5cd4.init_networkdriver()] getting iface addr  
  4. [/var/lib/docker|116d5cd4] -job init_networkdriver() = OK (0)  

看样子只是简单的创建了bridge docker0,分配ip地址。其实内部还是有比较复杂的操作,图示如下: 



Init bridge

这里有两个相关的启动参数: 

[html]  view plain copy print ?
  1. -b, --bridge="": Attach containers to a pre-existing network bridge; use 'none' to disable container networking  
  2. --bip="": Use this CIDR notation address for the network bridge's IP, not compatible with -b  

第一个是指定bridge接口,第二个指定bridge IP,两个参数不能共存。这就是说,指定bridge IP只能针对默认的docker0。第一个参数如果不指定,那么默认是用docker0, 第二个参数不指定,默认用docker内部定好的IP,一般没冲突的话,就会是第一个172.17.42.1 。

这里做了什么?

  1. 查看指定(或者默认)的bridge口是否存在。
  2. 如果不存在,则创建默认bridge0口(不指定-b参数)。

逻辑上非常简单,这里就不再深入展开了,细节可以去看代码。插一句嘴,实际创建bridge0口的操作是通过golang的syscall库进行的: 

[html]  view plain copy print ?
  1. if _, _, err :syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), SIOC_BRADDBR, uintptr(unsafe.Pointer(nameBytePtr))); err != 0 {  
  2.     return err  
  3.     }  


Init iptables

这里直接看看启动完后,iptalbes里面有什么变化,就了解Docker干了什么了: 

[html]  view plain copy print ?
  1. $ sudo iptables -L --verbose  
  2. Chain INPUT (policy ACCEPT 168 packets, 11814 bytes)  
  3.  pkts bytes target     prot opt in     out     source               destination           
  4.   
  5. Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)  
  6.  pkts bytes target     prot opt in     out     source               destination           
  7.     0     0 ACCEPT     all  --  any    docker0  anywhere             anywhere             ctstate RELATED,ESTABLISHED  
  8.     0     0 ACCEPT     all  --  docker0 !docker0  anywhere             anywhere              
  9.     0     0 ACCEPT     all  --  docker0 docker0  anywhere             anywhere              
  10.   
  11. Chain OUTPUT (policy ACCEPT 128 packets, 16744 bytes)  
  12.  pkts bytes target     prot opt in     out     source               destination   
  13.   
  14. $ sudo iptables -L -t nat --verbose  
  15. Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)  
  16.  pkts bytes target     prot opt in     out     source               destination           
  17.     0     0 DOCKER     all  --  any    any     anywhere             anywhere             ADDRTYPE match dst-type LOCAL  
  18.   
  19. Chain INPUT (policy ACCEPT 0 packets, 0 bytes)  
  20.  pkts bytes target     prot opt in     out     source               destination           
  21.   
  22. Chain OUTPUT (policy ACCEPT 11 packets, 1014 bytes)  
  23.  pkts bytes target     prot opt in     out     source               destination           
  24.     0     0 DOCKER     all  --  any    any     anywhere            !127.0.0.0/8          ADDRTYPE match dst-type LOCAL  
  25.   
  26. Chain POSTROUTING (policy ACCEPT 11 packets, 1014 bytes)  
  27.  pkts bytes target     prot opt in     out     source               destination           
  28.     0     0 MASQUERADE  all  --  any    any     172.17.0.0/16       !172.17.0.0/16             
  29.   
  30. Chain DOCKER (2 references)  
  31.  pkts bytes target     prot opt in     out     source               destination  


先来看filter表,docker启动后在FORWARD chain里面增加了三条规则。

1. 第一条规则表示从其他接口发往docker0(就是发往container)的报文,只有RELATED和ESTABLISHED状态能通过。(比如ftp的关联链接,container主动建链的TCP链接。外部是不能主动跟container内部的服务建链的。)

2. 第二条和第三条表示从docker0发往其他端口的报文都能通过。


再来看看nat表,这里比较重要。

1. PREROUTING chain中和OUTPUT chain增加了都一条规则,表示在收发两端,查找route之前,如果报文地址类型是local,就会送到DOCKER chain处理。初始化时DOCKER chain是空的,后面再详细介绍这两条规则的作用。(在route之前怎么知道目的地址是local?这里需要再研究下。)

2. 在POSTROUTING chain中加了一条规则: 表示从container往host外发(不是container之间)的报文,都会将报文原地址改为外发接口地址发出去。这样在container访问host外部网络时,外部看到的其实只是host地址,看不到container的私有地址。


Init IP forward

这个比较简单,就是将proc设置打开:/proc/sys/net/ipv4/ip_forward 。打开了之后,报文就能够在host上的各个接口之间转发。


Register function

Docker内部的各种功能是以Job形式存在,这里注册的就是network相关的几个Job,在后面container的启动中会用到这些Job。 

[html]  view plain copy print ?
  1. "allocate_interface": Allocate,  
  2. "release_interface":  Release,  
  3. "allocate_port":      AllocatePort,  
  4. "link":               LinkContainers,  

到这里,Docker的网络初始化就介绍完了。


Container默认网络

默认网络,指的是在启动container时不加其他网络相关的参数,比如直接用 $docker run -i -t image command 运行启动的container。这个container默认将使用bridge模式的网络,链接到docker0。

内部流程主要分为两个部分,docker engine部分和execdriver部分,即配置部分和实际执行部分。 


这里先介绍bridge模式,其他模式会在后面的参数介绍中简单介绍。



Bridge 网络模式初始化

主要分为两部分:

1. allocate_interface

    前面docker daemon初始化最后,register function操作中注册了 “allocate_interface” Job,这里主要就是调用这个方法。

    这个方法比较简单,主要就是分配IP(在bridge0的网络地址范围中递增分配,不会重用已释放的IP。)、配置掩码、网关等,保存到container.NetworkSettings结构。这里还没实际的创建接口。

     逻辑简单,更细节的东西可以去看看代码。

2. create_interface

    这里是真正的创建网络接口,所以与所使用的execdriver强相关(lxc或libcontainer)。 这里先介绍lxc的实现,因为简单。

    Lxc的网络怎么配?可以用man查看帮助:$man lxc.conf , 里面专门有一节讲解怎么配container的网络。 

[html]  view plain copy print ?
  1. NETWORK  
  2.     The network section defines how the network is virtualized in the container. The network  virtualization  acts  
  3.     at  layer  two. In order to use the network virtualization, parameters must be specified to define the network  
  4.     interfaces of the container. Several virtual interfaces can be assigned and used in a container  even  if  the  
  5.     system has only one physical network interface.  
  6.   
  7.          lxc.network.type  
  8.            specify  what kind of network virtualization to be used for the container. Each time a lxc.network.type  
  9.            field is found a new round of network configuration begins. In this way, several network virtualization  
  10.            types  can be specified for the same container, as well as assigning several network interfaces for one  
  11.   
  12.            container. The different virtualization types can be:  
  13.   
  14.            empty: will create only the loopback interface.  

    Docker在这里也是用lxc的配置,不过配置文件是动态生成的。可以参看 lxc_template.go里面的配置文件: 

[html]  view plain copy print ?
  1. const LxcTemplate = `  
  2. {{if .Network.Interface}}  
  3. # network configuration  
  4. lxc.network.type = veth  
  5. lxc.network.link = {{.Network.Interface.Bridge}}  
  6. lxc.network.name = eth0  
  7. lxc.network.mtu = {{.Network.Mtu}}  
  8. {{else if .Network.HostNetworking}}  
  9. lxc.network.type = none  
  10. {{else}}  
  11. # network is disabled (-n=false)  
  12. lxc.network.type = empty  
  13. lxc.network.flags = up  
  14. lxc.network.mtu = {{.Network.Mtu}}  
  15. {{end}}  

有了配置文件,通过lxc-start就能启动一个带网络的container了,container里面的接口链接到bridge0。

注:这里涉及到network namespace和bridge的知识,这里不扩展介绍了,不然写不完了。


Container端口映射

如果host外部客户想访问container里面的服务,怎么办? 从前面介绍的docker daemon网络初始化看到,docker添加了一条iptables规则,外部发往container的报文,只有在状态是RELATED和ESTABLISHED链接中,才能通过。那么外部客户主动向container服务发起的建链,是会被拒绝的。

Docker官网推荐的方式是使用端口映射,对外暴露的是host IP:port,由host做NAT转换为container内部的IP和端口。具体是怎么实现的呢?我们来看一下。

如果我们这样创建一个container: $docker run -i -t -p 6000 image command ,然后看看iptables发生了什么变化(省略了一些没影响的Chain): 

[html]  view plain copy print ?
  1. <span style="color:#444444;">$ sudo iptables -L --verbose  
  2. Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)  
  3.  pkts bytes target     prot opt in     out     source               destination           
  4.     </span><span style="color:#ff0000;">0     0 ACCEPT     tcp  --  !docker0 docker0  anywhere             172.17.0.2           tcp dpt:x11</span><span style="color:#444444;">  
  5.     0     0 ACCEPT     all  --  any    docker0  anywhere             anywhere             ctstate RELATED,ESTABLISHED  
  6.     0     0 ACCEPT     all  --  docker0 !docker0  anywhere             anywhere              
  7.     0     0 ACCEPT     all  --  docker0 docker0  anywhere             anywhere  
  8.   
  9. $ sudo iptables -L -t nat --verbose  
  10. Chain POSTROUTING (policy ACCEPT 18 packets, 1486 bytes)  
  11.  pkts bytes target     prot opt in     out     source               destination           
  12.     0     0 MASQUERADE  all  --  any    any     172.17.0.0/16       !172.17.0.0/16               
  13.   
  14. Chain DOCKER (2 references)  
  15.  pkts bytes target     prot opt in     out     source               destination           
  16.     </span><span style="color:#ff0000;">0     0 DNAT       tcp  --  !docker0 any     anywhere             anywhere             tcp dpt:49153 to:172.17.0.2:6000</span>  


这里就很明显了,里面主要添加了两条规则:

1. 在filter表FORWARD链中添加一条规则,让从host外部过来的报文能够进入container(这里172.17.0.2是新建的container的IP,x11其实就是端口6000,这是个知名端口)。

2. 在nat表DOCKER链中添加一条规则,host外部过来的访问49153端口的报文,会做DNAT将目的IP和端口转换为172.17.0.2:6000。

有了这两条规则,外部进来的报文就能正常访问container内部6000端口的服务了。


Container之间通信

同一个host上的container,默认是可以相互通信的,原理就是linux的bridge原理。这里要介绍的其实是禁止container之间相互通信。 

[html]  view plain copy print ?
  1. --icc=true                                   Enable inter-container communication  

上面这个参数是启动docker daemon时可选的参数,默认是true,如果设置为false,那么这个host上的container就不能相互通信了,这其实也是通过iptables来实现的。 

[html]  view plain copy print ?
  1. <span style="color:#444444;">$ sudo iptables -L --verbose  
  2. Chain INPUT (policy ACCEPT 94 packets, 6424 bytes)  
  3.  pkts bytes target     prot opt in     out     source               destination           
  4.   
  5. Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)  
  6.  pkts bytes target     prot opt in     out     source               destination           
  7. </span><span style="color:#ff0000;">    0     0 DROP       all  --  docker0 docker0  anywhere             anywhere</span><span style="color:#444444;">              
  8.     0     0 ACCEPT     all  --  any    docker0  anywhere             anywhere             ctstate RELATED,ESTABLISHED  
  9.     0     0 ACCEPT     all  --  docker0 !docker0  anywhere             anywhere              
  10.   
  11. Chain OUTPUT (policy ACCEPT 62 packets, 7000 bytes)  
  12.  pkts bytes target     prot opt in     out     source               destination</span>  

这里在FORWARD chain里面增加了一条规则,如果报文是在docker0这个bridge内部通信,那么就丢弃。这样就禁止了container之间的直接通信。


小结

以上总结了Docker默认网络模式与常用网络配置。但其实从Docker选项和代码中可以看到,还有些网络相关的地方没有介绍到,要么是我没有用过就没分析、要么是我还没看懂的,比如docker run时可以用 -net 参数指定的host网络模式、container网络模式(这两个初步看,一个是直接用host的namespace,一个是用指定container的namespace,似乎很简单),还有内部的proxy(似乎是用于container通过host映射的端口访问另一个container的服务,这个还没怎么看懂。)。 后面分析之后会更新

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐