很多人可能在项目中已经使用docker很长时间,但是却很少有人知道docker的网络是如何实现的。我应该就算是很多人中的一个。
开始前有一点需要注意的是:如果你现在正使用的是docker for mac,建议你还是在mac上安装vagrant,然后使用vagrant开启一台linux虚拟机,然后在这台虚拟机上安装docker for linux。这样我们的学习环境也更加的贴近生产环境。
docker0网桥
当在一台未经特殊网络配置的ubuntu机器上安装完docker之后,在宿主机上通过使用ifconfig
命令可以看到多了一块名为docker0的网卡:
vagrant@vagrant-ubuntu-trusty-64:~$ ifconfig
docker0 Link encap:Ethernet HWaddr 02:42:b6:12:b7:33
inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:b6ff:fe12:b733/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:45 errors:0 dropped:0 overruns:0 frame:0
TX packets:18 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:3044 (3.0 KB) TX bytes:1460 (1.4 KB)
eth0 Link encap:Ethernet HWaddr 08:00:27:36:92:90
inet addr:10.0.2.15 Bcast:10.0.2.255 Mask:255.255.255.0
inet6 addr: fe80::a00:27ff:fe36:9290/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:503098 errors:0 dropped:0 overruns:0 frame:0
TX packets:236748 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:397962985 (397.9 MB) TX bytes:14862746 (14.8 MB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
可以看到此docker0网卡的IP为172.17.0.1/16。有了这样一块网卡,宿主机也会在内核路由表上添加一条到达相应网络的静态路由,可以通过route -n
查看。
vagrant@vagrant-ubuntu-trusty-64:~$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 eth0
10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
当一宿主机要发送数据时,它会查自己的内核路由表,找到一条最精确匹配目标主机IP地址的路由来转发数据。
我们知道,同一网段内发送数据是不需要经过网关的,而不同网段之间发送数据必需经过网关。
我们的宿主机有三张网卡:
lo(127.0.0.1
),本机回环网卡;
eth0(10.0.2.15
)在10.0.2.0/24
网段;
docker0(172.17.0.1
)在172.17.0.0/16
网段;
假如:
Destination IP是
10.0.2.13
,则会匹配到第二条路由。即所有目的IP为10.0.2.0/24
网络的数据包从eth0网卡发出。Gateway为0.0.0.0
表示不经过网关,这个很好理解,因为同一网段内发送数据不需要经过网关。Destination IP是
172.17.0.3
,则会匹配到第三条路由。即所有目的IP为172.17.0.0/16
网络的数据包从docker0网卡发出。Destination IP是其它,比如
32.10.2.0
,则会匹配到第一条路由:默认路由。即数据包从eth0发出,而发往外网的数据包需要经过网关转发,所发配置了Gateway为10.0.2.2
。
现在使用docker run
创建一个docker容器。
sudo docker run -it --name myubuntu ubuntu /bin/bash
然后在容器中执行ifconfig
,如果你发现你的容器中没有ifconfig
命令,可以执行以下命令安装net-tools:
root@bde36ed1f506:/# apt-get update
root@bde36ed1f506:/# apt-get install net-tools
// 顺带也把iputils-ping装了
root@bde36ed1f506:/# apt-get install iputils-ping
然后再执行ifconfig
:
root@bde36ed1f506:/# ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:11218 errors:0 dropped:0 overruns:0 frame:0
TX packets:11128 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:24859036 (24.8 MB) TX bytes:606400 (606.4 KB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
在myubuntu容器中有两块网卡:eth0和lo。eth0为容器与外界通信的网卡。而且eth0(172.17.0.2
)与宿主机中的docker0(172.17.0.1
)在同一个网段。
现在我们查看myubuntu的路由表:
root@bde36ed1f506:/# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
可以发现myubuntu的默认网关正是宿主机的docker0网卡。我们刚才install了那么多package说明容器是可以访问到外网的,说明容器的eth0与宿主机的docker0网卡是互通的。
现在我们回到宿主机的console,执行ifconfig
:
vagrant@vagrant-ubuntu-trusty-64:~$ ifconfig
...
vethee89fa0 Link encap:Ethernet HWaddr 36:32:c3:bf:5e:78
inet6 addr: fe80::3432:c3ff:febf:5e78/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:11330 errors:0 dropped:0 overruns:0 frame:0
TX packets:11422 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:619186 (619.1 KB) TX bytes:26177038 (26.1 MB)
会发现有一块以"veth"开头的网卡,如上:vethee89fa0。我们可以猜测这块网卡是veth设备,而veth pair总是成对出现的,很多人可能不知道veth设备是什么。
veth pair是用于不同network namespace间进行通信的方式,veth pair将一个network namespace数据发往另一个network namespace的veth。
如下:
如果多个network namespace需要进行通信,则需要借助bridge:
现在我们知道, veth pair通常用来连接两个network namespace,那么另一个应该就是docker容器中的eth0了。之前我们已经知道myubuntu容器的eth0和宿主机的docker0是相连的,那么vethee89fa0也应该是与docker0相连的。那么,现在来看docker0就不只是一个简单的网卡设备了,而是一个网桥。
真实情况正是如此,下图即为docker默认网络模式(bridge模式)下的网络环境拓扑图:
我们在宿主机上安将docker时会添加一个docker0的网卡,当我们使用docker run创建一个容器,并且没有指定网络模式的时候,会使用默认网络模式,创建出一个docker0网桥,并以veth pair连接各容器的网络,容器中的数据通过docker0网桥转发到宿主机的eth0网卡上。
在linux中,可以使用brctl
命令查看和管理网桥(需要安装bridge-utils软件包),如查看本机上的linux网桥以及其上的端口:
vagrant@vagrant-ubuntu-trusty-64:~$ sudo brctl show
bridge name | bridge id | STP enabled | interfaces |
---|---|---|---|
docker0 | 8000.0242b612b733 | no | vethee89fa0 |
如果我再使用docker run
再创建一个docker容器后再查看linux网桥以及其上的端口:
vagrant@vagrant-ubuntu-trusty-64:~$ sudo brctl show
bridge name | bridge id | STP enabled | interfaces |
---|---|---|---|
docker0 | 8000.0242b612b733 | no | vethee89fa0 |
veth0c3e53a |
会发现又多了一双veth pair连接到了docker0网桥。
现在我们再来看这个网桥,我们会觉得它就像是一个交换机,为连在其上的设备转发数据帧。网桥上的veth网卡设备相当于交换机上的端口,可以将多个容器或虚拟机连接在其上,这些端口工作在二层,所以是不需要配置IP信息的。图中docker0网桥变为连在其上的容器转发数据帧,使得同一台宿主机上的docker容器之间可以相互通信。也许你应该已经注意到docker0既然是二层设备,其上怎么也配置了IP呢? docker0是普通的Linux网桥,它是可以在上面配置IP的,可以认为其内部有一个可以用于配置IP信息的网卡接口。那么为什么要为它配置IP呢?在Docker的桥接网络模式中,docker0的IP地址作为连接于之上的容器的默认网关存在。
iptables规则
Docker安装完成后,默认会在宿主机上增加一些iptables规则,以用于docker容器和容器之间以及和外界有通信,可以使用iptables-save
命令查看规则。
vagrant@vagrant-ubuntu-trusty-64:~$ sudo iptables-save
# Generated by iptables-save v1.4.21 on Sun Jun 18 08:24:03 2017
*nat
:PREROUTING ACCEPT [4:1276]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [480:28821]
:POSTROUTING ACCEPT [480:28821]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 3306 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 3306 -j DNAT --to-destination 172.17.0.3:3306
COMMIT
# Completed on Sun Jun 18 08:24:03 2017
# Generated by iptables-save v1.4.21 on Sun Jun 18 08:24:03 2017
*filter
:INPUT ACCEPT [2374:4027249]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [1986:129221]
:DOCKER - [0:0]
:DOCKER-ISOLATION - [0:0]
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3306 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
COMMIT
# Completed on Sun Jun 18 08:24:03 2017
可以看到nat表上的POSTROUTING链有这么一条规则:
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
这条规则关系着docker容器和外界的通信,含义是将源地址为172.17.0.0/16
的数据包(即Docker容器发出的数据),当不是从docker0网卡发出时做SNAT(源地址转换,将IP包的数据地址替换为相应网卡的地址)。这样一来,从Docker容器访问外网的流量,在外部看来就是从宿主机上发出的,外部感觉不到Docker容器的存在。
在iptables中可以灵活的做各种网络地址换(NAT),我想很多人可能不是太了解NAT(Network address translation)。
网络地址转换主要有两种: SNAT(source NAT)和DNAT(destination NAT)。
SNAT,即源地址目标转换。比如,多个PC机使用ADSL路由器共享上网。每个PC机都配置了内网IP,PC机访问外部网络的时候,路由器将数据包的报头中的源地址替换成路由器的IP,当外部网络的服务器,比如,网站web服务器接到访问请求的时候,他的日志记录下来的是路由器的IP地址,而不是PC机的内网IP。
这是因为,这个服务器收到的数据包的报头里边的源地址已经被换了,所以叫做SNAT,基于源地址的地址转换。
DNAT,即目标地址转换。典型的应用是,有个web服务器放在内网,前端有个防火墙配置公网IP。互联网上的访问者使用公网IP来访问这个网站。当访问的时候,客户端发出一个数据包,这个数据包的报头里边,目标地址写的是防火墙的公网IP。防火墙会把这个数据包的报头改写一次,将目标地址改写成web服务器的内网IP,然后再把这个数据包发送到内网的web服务器上。
MASQUERADE,地址伪装,在iptables中有着和SNAT相近的效果,但也有一些区别。
使用SNAT的时候,即可以NAT成一地址也可以NAT成多个地址,但是,对于SNAT,不管是几个地址,必须明确的指定要SNAT的IP。假如当前系统用的是ADSL动态拨号方式,那么每次拨号,出口IP都会改变,这个时候如果按照现在的方式来配置iptables就会出现问题。因为每次拨号后,服务器地址都会变化,而iptables规则内的ip是不会随着自动变化的。每次地址变化后都必须手工修改一次iptables,把规则里边的固定IP改成新的IP,这样很不方便。
MASQUERADE就是针对这种场景而设计的,他的作用是,从服务器的网卡上,自动获取当前IP地址来做NAT。
比如下边的命令:
iptables -t nat -A POSTROUTING -s 10.8.0.0/255.255.255.0 -o eth0 -j MASQUERADE
如此配置的话,不用指定SNAT的目标IP了。不管现在eth0的出口获得了怎样的动态IP,MASQUERADE会自动读取eth0现在的IP地址然后做SNAT。这样就很好的实现了动态SNAT地址转换。