![v2-c0b88641afa253831150c9dc7fc4e6e4_1440w.jpg?source=172ae18b](http://img-03.proxy.5ce.com/view/image?&type=2&guid=83990df9-182a-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-c0b88641afa253831150c9dc7fc4e6e4_1440w.jpg?source=172ae18b)
我学习理论知识的时候都必须实践一下才能记得住。特别是对于计算机网络这种极其抽象的东西,不去实践一下,我会感到很虚。所以,我写的手撕系列的文章都比较重实践,里面介绍的知识在网上都有大量总结,我的目的是并不是介绍这些知识点,而是通过亲手实践一遍,在脑海中留下更清晰的知识脉络,也希望大家能够跟着我做一遍掌握其中细节。
这篇文章是手撕docker网络的第二篇。上一篇中我们介绍了linux的network namespace,veth pair,bridge的概念,并且通过这些虚拟设备我们搭建出了一个虚拟网络。这一篇我们主要分析docker的bridge模式下,container与container之间,container与host之间,container与外网之间的连接原理。通过实例挖掘出docker自动创建出的网络空间,veth pair以及bridge等linux虚拟网络设备,并分析它们的作用。
由于本人对linux虚拟网络理解有限,本文也是我的学习总结,如果出现错误希望大家在评论出指出!
理解本文需要一些前置知识:
- linux的network namespace,veth pair,bridge,可以参考我的上一篇文章:
![v2-f5c85fd37b21a4d62ec015bc0520f13d_180x120.jpg](http://img-03.proxy.5ce.com/view/image?&type=2&guid=83990df9-182a-eb11-8da9-e4434bdf6706&url=https://pic2.zhimg.com/v2-f5c85fd37b21a4d62ec015bc0520f13d_180x120.jpg)
- linux的netfilter框架以及iptables的基础知识,推荐朱双印大佬的博客:
![v2-73e11e98881f07db72ae98728a30a137_ipico.jpg](http://img-02.proxy.5ce.com/view/image?&type=2&guid=83990df9-182a-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-73e11e98881f07db72ae98728a30a137_ipico.jpg)
Docker网络模式介绍
首先我们来看看docker都有哪些网络模式,目前为止,除了第三方网络插件以外,docker提供了5种网络模式:
- bridge模式:bridge模式也是默认的网络模式。该模式使用linux的network namespace对container的网络空间进行隔离,并通过虚拟网桥链接不同container的网络空间,让container之间能够相互连通。由于网络空间被隔离,每个container都拥有不同的私网IP,多个container使用相同端口不会发生端口冲突。container与外网连通需要通过NAT。
- host模式:host模式下,container会共享主机的网络空间。container的使用的IP和端口即是主机的IP和端口,多个container使用相同端口会发生端口冲突。好处是container直接使用主机IP和端口,从外网连接container无需NAT。
- macvlan模式:macvlan 是一种网卡虚拟化技术,能够将一张网卡虚拟出多张网卡。macvlan有四种通信模式,常用模式是 bridge。通过macvlan将主机网卡分身出多个虚拟网卡,并赋予每个container一个虚拟网卡,实现同台host上的container之间的互通。此外,通过特定网络设备直接路由,打造出互通的大二层网络,实现container的跨主机点到点的之间通信。
- overlay模式:Overlay网络是指在不改变现有网络基础设施的前提下,通过某种约定通信协议,把二层报文封装在IP报文之上的新的数据格式。这样不但能够充分利用成熟的IP路由协议进程数据分发;而且在Overlay技术中采用扩展的隔离标识位数,能够突破VLAN的4000数量限制支持高达16M的用户,并在必要时可将广播流量转化为组播流量,避免广播数据泛滥。
本文关注的重点则是bridge模式。
环境准备
1. 准备一台ubuntu主机。我在AWS开了一台EC2(Ubuntu 18.04.5),如果你没有AWS账号也可以开一台虚拟机。AWS的EC2也就比虚拟机多了一个公网IP而已。
2. 安装Docker,ubuntu参考Install Docker Engine on Ubuntu。
3. 启动2个nginx container A和B,使用主机的8080与8081端口进行端口转发。我选择的nginx镜像是nginxdemos/hello:plain-text
,访问该镜像会返回nginx server地址,server名字,日期,访问路径等信息,比较方便。
$ docker run -p 8080:80 -d nginxdemos/hello:plain-text
6e31e10d5fac2925d7f0facc1d0c327c00a512793733add1c84a4edbb4fe9abc
$ docker run -p 8081:80 -d nginxdemos/hello:plain-text
a7c6786f681c3ec849f32b713c8de5b3b66adc808c86a6f1edf96acf9286524e
$ curl localhost:8080
Server address: 172.17.0.2:80
Server name: 6e31e10d5fac
Date: 31/Aug/2020:02:05:34 +0000
URI: /
Request ID: 8f27412fdf8a01bfde48c12b1ba2b702
$ curl localhost:8081
Server address: 172.17.0.3:80
Server name: a7c6786f681c
Date: 31/Aug/2020:02:08:13 +0000
URI: /
Request ID: cb28edefd29a5e4e71f1374ed88857f7
4. 将docker的网络命名空间路径链接到ip netns
管理的命名空间路径上,方便后期用ip netns
命令进行操作。这步操作是因为ip netns
命令管理的命名空间在/var/run/netns/
路径下,而docker管理的命名空间在/var/run/docker/netns
路径下,为了直接用ip netns
命令管理docker的命名空间,所以需要将这2个文件夹链接起来。
$ ln -s /var/run/docker/netns /var/run
$ ls -l /var/run/netns
lrwxrwxrwx 1 root root 21 Aug 31 01:21 /var/run/netns -> /var/run/docker/netns
5. 查看docker创建的网络命名空间,以及它们的网卡信息。我们看到docker创建了2个命名空间6e9269a3edd8和332acf6a7ea2,6e9269a3edd8的eth0的IP为172.17.0.3,而332acf6a7ea2的eth0的IP为172.17.0.2。从IP地址判断出332acf6a7ea2是container A的网络命名空间,6e9269a3edd8是container B的。
$ ip netns
6e9269a3edd8 (id: 1)
332acf6a7ea2 (id: 0)
$ ip netns exec 6e9269a3edd8 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
$ ip netns exec 332acf6a7ea2 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
至此,我们的环境准备完毕。IP地址,网络命令空间,以及container之间的关系可以总结如图:
![v2-7d7b89bc69eb41985fd1714a5ea0de2c_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=83990df9-182a-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-7d7b89bc69eb41985fd1714a5ea0de2c_b.jpg)
bridge网络拆解
1. container之间的网络连接
我们知道veth总是成对出现的,如何判断是否是一对就要看网卡名称后面@if后的编号,一对veth的编号之间的差为1。
在环境准备的过程中,我们发现在container A的网络空间中332acf6a7ea2的veth为eth0@if5,编号为5;在container B的网络空间6e9269a3edd8中有eth0@if7,编号为7。在host中查看网卡:
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
link/ether 06:41:91:32:f1:84 brd ff:ff:ff:ff:ff:ff
inet 172.31.47.15/20 brd 172.31.47.255 scope global dynamic eth0
valid_lft 3414sec preferred_lft 3414sec
inet6 fe80::441:91ff:fe32:f184/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:32:e7:c7:63 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:32ff:fee7:c763/64 scope link
valid_lft forever preferred_lft forever
5: vethdde3857@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether c2:72:f4:df:c3:1d brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::c072:f4ff:fedf:c31d/64 scope link
valid_lft forever preferred_lft forever
7: veth0304949@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 6e:6d:41:a7:b1:60 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::6c6d:41ff:fea7:b160/64 scope link
valid_lft forever preferred_lft forever
可以发现host中有一个名为docker0的网卡,docker0还是一个bridge,此外还有2个veth,vethdde3857@if4编号为4,veth0304949@if6编号为6。查看bridge的链接状况:
$ bridge link
5: vethdde3857 state UP @if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 state forwarding priority 32 cost 2
7: veth0304949 state UP @if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 state forwarding priority 32 cost 2
vethdde3857@if4与container A网络空间的eth0@if5是一对,而veth0304949@if6与container B网络空间的eth0@if7是一对。其中vethdde3857@if4与veth0304949@if6连接着docker0。
目前为止的网络拓扑图:
![v2-aa5ce567b92b434985e16de87a035f32_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=83990df9-182a-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-aa5ce567b92b434985e16de87a035f32_b.jpg)
现在从Container A(netns: 332acf6a7ea2, ip: 172.17.0.2)发往B(netns: 6e9269a3edd8, ip:172.17.0.3)一个请求:
$ ip netns exec 332acf6a7ea2 curl 172.17.0.3:80
Server address: 172.17.0.3:80
Server name: a7c6786f681c
Date: 31/Aug/2020:09:13:19 +0000
URI: /
Request ID: db3d75fb513142e38a1940b36abd9025
我们来追踪下这个数据包达到B之前都经历了什么:
首先从A发网B的数据包的源IP为 172.17.0.2,源端口为一随机端口xx, 目的IP为172.17.0.3,目的端口为80,即:
数据包:172.17.0.2:xx -> 172.17.0.3:80
再数据包从Container A的网络空间332acf6a7ea2离开之前,需要先查询332acf6a7ea2的路由表:
$ ip netns exec 332acf6a7ea2 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
可以看到我们的数据包命中第二条路由,gateway是0.0.0.0表面目的地与本机也就是现在的netns是之间相连的。通过ARP协议,可以找到目的IP地址172.17.0.3的MAC地址,也就是container B的网络空间中的eth0的网卡MAC地址。
注意,在数据包从Container A的网络空间332acf6a7ea2离开之前本来还需要经过netfilter的OUTPUT链和POSTROUTING链的检查,但container的网络空间中的所有的链都是空的,所以不对这些链的检查进行讨论。
得知目的地的MAC地址后,数据包从332acf6a7ea2的eth0网卡发出,通过veth pair达到docker0网桥。docker0网桥位于主机的网络空间中,而数据包来源于332acf6a7ea2网络空间,所以属于外部发往本机进程的数据包,需要先经过主机网络空间的PREROUTING的检查,我们来看看PREROUTING链都有啥,PREROUTING涉及到三张表:raw,mangle与nat,其中raw表和mangle表都是空的,这里不讨论,我们主要看看nat表:
$ iptables -t nat -vnL
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 LOG all -- * * 172.17.0.2 0.0.0.0/0 LOG flags 0 level 4 prefix "Before nat PREROUTING: "
3420 160K DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
...
其中第一条target为LOG的规则是我为了观察数据包的内容,在PREROUTING链中手动插入一条LOG规则,所有源IP为172.17.0.2的数据包将被打印在内核日志中:
$ iptables -t nat -I PREROUTING -j LOG -s 172.17.0.2 --log-prefix="Before nat PREROUTING: "
第二条target为DOCKER的规则是docker自己创建,我们一会分析它的作用。
为了看看到达的数据包的内容,我们再触发一次请求,查看内核地址,我们发现:
$ dmesg | grep SRC=172.17.0.2
[28987.249370] Before nat PREROUTING: IN=docker0 OUT= PHYSIN=vethdde3857 MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=18872 DF PROTO=TCP SPT=41046 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
可以看到,从“IN=docker0 PHYSIN=vethdde3857”得知,该数据包是从docker0网卡进入的,而真正的物理进入点是vethdde3857,也就是链接着container A命名空间与docker0的veth pair在docker0端的veth。
接下来我们看看nat的PREROUTING的第二条规则,写着目的地址是127.0.0.1/8以外,且类型是LOCAL的数据包都将发往DOCKER这条自定义链。什么是LOCAL类型的地址?可以参考以下链接:WTF addrtype in iptables manpage 与 Docker's NAT table output chain rule
简单来讲LOCAL地址类型指的是本机的网卡所具有的地址,可以通过以下命令查看:
$ ip route show table local type local
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
local 172.31.47.15 dev eth0 proto kernel scope host src 172.31.47.15
可以看到我们的数据包的目的地址172.17.0.2不属于LOCAL地址类型,所以不会命中PREROUTING的第二条规则。所以到此PREROUTING链的检查结束。
紧接着,docker0网桥检查数据包的目的地的MAC地址,发现并不是发给本机的,于是进行转发。由于数据包要进行转发,所以还需要经过FORWARD链与POSTROUTING链的检查。FORWARD链涉及到2张表:mangle和filter,其中mangle表是空的,所以我们只需要看filter表的FORWARD链:
$ iptables -vnL
...
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
6 394 LOG all -- * * 172.17.0.2 0.0.0.0/0 LOG flags 0 level 4 prefix "Before filter FORWARD: "
479 40660 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
479 40660 DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
224 18580 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
66 3416 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
189 18664 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
7 444 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0 0.0.0.0/0 0.0.0.0/0
...
同样的为了观察docker0转发后的数据包,我在filter中插入了一条LOG规则,也就是上面显示的第一条规则:
$ iptables -I FORWARD -j LOG -s 172.17.0.2 --log-prefix="Before filter FORWARD: "
再触发一次请求,我们先来看看docker0转发后的数据包长什么样:
$ dmesg | grep SRC=172.17.0.2
[30418.994388] Before filter FORWARD: IN=docker0 OUT=docker0 PHYSIN=vethdde3857 PHYSOUT=veth0304949 MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=13728 DF PROTO=TCP SPT=41048 DPT=80 WINDOW=501 RES=0x00 ACK URGP=0
可以看到“IN=docker0 OUT=docker0 PHYSIN=vethdde3857 PHYSOUT=veth0304949”,数据包的OUT网卡被填上了,OUT与IN一样是docker0,而PHYSOUT却是veth0304949,也就是链接container B与docker0的veth pair再docker0端的veth。
filter的FORWARD表中,除了我们插入的这条LOG规则以外,我们还可以看到docker定义的2条规则,2条规则都会匹配所有的数据包,第一条规则的target是DOCKER-USER:
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
469 39695 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
在filter的DOCKER-USER链中,并没有做任何处理,直接RETURN了。我们来看看filter的FORWARD的第二条规则,它的target是DOCKER-ISOLATION-STAGE-1:
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
pkts bytes target prot opt in out source destination
189 18664 DOCKER-ISOLATION-STAGE-2 all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
469 39695 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
这里由于我们的数据包IN和OUT都是docker0,所以不会匹配到filter的DOCKER-ISOLATION-STAGE-1的第一条规则,该规则要求IN为docker0而OUT为docker0以外的网卡。所以我们的数据包在这里命中第二条直接RETURN到了上一层,也就是filter的FORWARD链,接着匹配剩下的规则。
数据包来到FORWARD的第4条规则,它接受所有发往docker0网卡的状态为RELATED与ESTABLISHED的请求。所以如果是已经建立的链接,在这条规则数据包就会被FORWARD接受,进入POSTROUTING链。但如果是第一次访问,我们的请求的状态是NEW,那么就不会命中这条规则。假设我们的请求是第一次发起的,状态是NEW,紧接着匹配第5条,第5条表示,所有发往docker0网卡的请求都会去DOCKER这条链继续匹配。我们来看看filter表的DOCKER链:
Chain DOCKER (1 references)
pkts bytes target prot opt in out source destination
55 2772 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.2 tcp dpt:80
7 340 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.3 tcp dpt:80
我们看到filter的DOCKER链中的2条规则都要求in不是docker0网卡,所以显然我们的数据包不会命中DOCKER链中的任意一条规则,我们又回到filter的FORWARD链继续匹配第6条,第6条要求out不是docker0,所以也不会命中。我们再看看第7条,第7条表示接受所有in和out都是docker0的数据包,终于在这一步我们的数据包被FORWARD链接受了。
这里值得注意的是,只有状态为NEW的请求才会走到FORWARD链的第7条记录,而状态为RELATED或者ESTABLISHED的请求会在第4条记录被接受。在TCP的3次握手过程中,当A发往B SYN请求时,该请求的状态是NEW。而在B回复A SYN ACK之后,请求的状态会被conntrack更新为ESTABLISHED,且conntrack会维持这个链接的状态一段时间。在A回复B ACK ACK的时候,由于请求状态已经是ESTABLISHED了,这条请求会在第4条记录被接受。
FORWARD链之后就是POSTROUTING链,POSTROUTING链涉及2个表mangle与nat,mange是张空表,我们来看nat的POSTROUTING链:
$ iptables -t nat -vnL
...
Chain POSTROUTING (policy ACCEPT 17 packets, 1201 bytes)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 172.17.0.2 172.17.0.2 tcp dpt:80
0 0 MASQUERADE tcp -- * * 172.17.0.3 172.17.0.3 tcp dpt:80
...
第一条规则要求out不是docker0网卡,直接pass。第二条和第三条规则分别匹配的是172.17.0.2发往172.17.0.2:80的数据包,和172.17.0.3发往172.17.0.3:80的数据包,即container A访问自己的nginx与contain B访问自己的nginx的请求,如果命中的话会给该请求进行MASQUERADE也就是SNAT转换。但我们的数据包从172.17.0.2发往172.17.0.3:80的,所以第二第三条都不会命中。我们看到nat的POSTROUTING的policy是ACCEPT,所以没有命中任何规则的数据包都会被接受。
到此为止我们的数据包经过了netfilter的重重检验,到达了veth0304949,然后通过veth pair传到了veth0304949的另一端 —— container B的网络空间中的eth0网卡。
container B的eth0收到数据包后,先检查MAC地址是不是自己的,再检查IP地址是不是自己的,最后发现目的端口为80,于是把数据包从内核空间传到用户空间给正在监听80端口的应用程序nginx,nginx收到数据包后,分析http首部以及包的内容,发现这是一个GET请求,请求的是“/”目录下的资源,于是返回资源给container A。数据包返回的过程也与上述分析类似,大家可以试试自己分析一遍。
2. container与host之间的网络连接
理解了container之间了链接的话,container与host的连接就很简单了。
我们来看看,从container A发起对host(本例中ip为172.31.47.15)的ping,都经历什么?
$ ip netns exec 332acf6a7ea2 ping -c 3 172.31.47.15
PING 172.31.47.15 (172.31.47.15) 56(84) bytes of data.
64 bytes from 172.31.47.15: icmp_seq=1 ttl=64 time=0.055 ms
64 bytes from 172.31.47.15: icmp_seq=2 ttl=64 time=0.078 ms
64 bytes from 172.31.47.15: icmp_seq=3 ttl=64 time=0.063 ms
--- 172.31.47.15 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2039ms
rtt min/avg/max/mdev = 0.055/0.065/0.078/0.011 ms
首先,数据包从container A的网络空间出发,首先查询路由表发现:
$ ip netns exec 332acf6a7ea2 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
发往172.31.47.15的数据包要经过gateway 172.17.0.1进行跳转。172.17.0.1也就是docker0的网卡IP,于是先通过ARP查询docker0的MAC地址。得知docker0的MAC IP后将数据包的目的MAC地址填上docker0的MAC地址起航。数据包经过PREROUTING链的部分与前文相同,不再赘述。
数据包经过veth pair到达docker0,docker0查询自己的路由表(即,host的路由表)发现172.31.47.15就在本地网络当中,但需要通过eth0网卡访问。
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.32.1 0.0.0.0 UG 100 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.31.32.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
172.31.32.1 0.0.0.0 255.255.255.255 UH 100 0 0 eth0
于是docker0将数据包递给eth0。eth0收到数据包后发现这个包就是发给自己的于是直接接受并且准备往进程发送。在数据包达到进程之前,还需要通过INPUT链的检测,不过INPUT链都是空的,所以畅通无阻地到达了进程。
3. container与公网之间的网络连接
接下来我们来分析下container A发往公网的数据包:
$ ip netns exec 332acf6a7ea2 ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=103 time=2.87 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=103 time=2.92 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=103 time=2.92 ms
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 2.878/2.907/2.924/0.065 ms
数据包从container A的网络空间到docker0网桥的流程,与container访问host的情况相同。不同的是,在数据包经过PREROUTING链之后,docker0检查路由表发现,去往8.8.8.8的数据包需要经过default gateway,且需要从eth0网卡出站。
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.32.1 0.0.0.0 UG 100 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.31.32.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
172.31.32.1 0.0.0.0 255.255.255.255 UH 100 0 0 eth0
此时docker0先将数据包转发给eth0网卡(这一步需要开启IP Forward),eth0网卡继续将数据包传递给gateway。由于数据包不是发给本机的,所以数据包在发往gateway之前还要经过FORWARD链与POSTROUTING链。
通过LOG操作,我们得知数据包在被FORWARD之前是:
[48899.144634] Before filter FORWARD: IN=docker0 OUT=eth0 PHYSIN=vethdde3857 MAC=02:42:32:e7:c7:63:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=8.8.8.8 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=27708 DF PROTO=ICMP TYPE=8 CODE=0 ID=3649 SEQ=1
可以看到这个数据包in网卡是docker0,而out网卡是eth0,匹配filter表的FORWARD链的第6条规则,被接受:
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
146 15807 LOG all -- * * 172.17.0.2 0.0.0.0/0 LOG flags 0 level 4 prefix "Before filter FORWARD: "
874 77525 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
874 77525 DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
391 33008 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
113 5960 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
370 38557 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
8 504 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
继续走到POSTROUTING链,POSTROUTING前的数据包为:
[49187.682425] Before nat POSTROUTING: IN= OUT=eth0 PHYSIN=vethdde3857 SRC=172.17.0.2 DST=8.8.8.8 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=919 DF PROTO=ICMP TYPE=8 CODE=0 ID=3655 SEQ=1
可以看到出站网卡为eth0且源IP为172.17.0.2,所以会命中nat的POSTROUTING的第二条规则,即所有源IP是172.17.0.0/16范围内,且出站网卡不是docker0的所有数据包:
Chain POSTROUTING (policy ACCEPT 6 packets, 641 bytes)
pkts bytes target prot opt in out source destination
5 420 LOG all -- * * 172.17.0.2 0.0.0.0/0 LOG flags 0 level 4 prefix "Before nat POSTROUTING: "
5 420 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 172.17.0.2 172.17.0.2 tcp dpt:80
0 0 MASQUERADE tcp -- * * 172.17.0.3 172.17.0.3 tcp dpt:80
数据包经过POSTROUTING,触发了MASQUERADE操作,数据包源IP被修改成eth0的IP。通过查看conntrack保持的链接记录,我们可以看到被SNAT后的数据包:
$ conntrack -L | grep 8.8.8.8
conntrack v1.4.4 (conntrack-tools): 7 flow entries have been shown.
icmp 1 27 src=172.17.0.2 dst=8.8.8.8 type=8 code=0 id=3698 src=8.8.8.8 dst=172.31.47.15 type=0 code=0 id=3698 mark=0 use=1
可以看到源IP从172.17.0.2被改成了172.31.47.15,也就是eth0网卡的IP。关于conntrack记录的格式可以参考文章:云计算底层技术-netfilter框架研究
数据包被SNAT后,由eth0发往default gateway,从而发往公网。
最后,我们来看看如果从公网访问我们的container A。在创建container A的时候我们通过-p 8080:80
选项将主机端口8080映射到了container A的80端口。这样发往主机8080端口的请求就会通过端口转发(DNAT)转发给container A的80端口。端口转发本身也是通过iptables和conntrack实现的。
本文的虚拟机是AWS的EC2,公网ip为3.113.17.15,我笔记本连的wifi ip地址为126.164.2.84。现在我们来看看走公网的数据包是怎么到达container A的,
# 在另一台主机上执行
$ curl 3.113.17.15:8080
Server address: 172.17.0.2:80
Server name: 6e31e10d5fac
Date: 31/Aug/2020:15:18:18 +0000
URI: /
Request ID: 8cc5f5e13d25a5b852f9851604cb4aa2
首先通过公网IP定位到我们的host,数据包在公网中的传递细节本文不做赘述,主要看看数据包到达host之后是怎么转发给container A的。
我们知道从外界发往主机进程的数据包首先要经过PREROUTING链。我们来看看host的nat表的PREROUTING规则(raw与mangle表为空,不考虑):
Chain PREROUTING (policy ACCEPT 32 packets, 1534 bytes)
pkts bytes target prot opt in out source destination
2 128 LOG all -- * * 126.164.2.84 0.0.0.0/0 LOG flags 0 level 4 prefix "Before nat PREROUTING: "
15 1188 LOG all -- * * 172.17.0.2 0.0.0.0/0 LOG flags 0 level 4 prefix "Before nat PREROUTING: "
6101 281K DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL ADDRTYPE match dst-type LOCAL
为了看到数据包长啥样,我又加了一条规则,来自我的ip(126.164.2.84)的数据包都打个LOG。让我们来看看这个数据包:
[51203.595339] Before nat PREROUTING: IN=eth0 OUT= MAC=06:41:91:32:f1:84:06:7a:78:95:2e:5a:08:00 SRC=126.164.2.84 DST=172.31.47.15 LEN=64 TOS=0x00 PREC=0x00 TTL=41 ID=0 DF PROTO=TCP SPT=64942 DPT=8080 WINDOW=65535 RES=0x00 SYN URGP=0
可以看到,我们的主机收到的数据包源IP是126.164.2.84,但目标IP却是172.31.47.15。但我们明明是用的公网IP 3.113.17.15啊,这是什么情况?我的猜想是这台EC2本身也处于一个内网当中,它的公网IP是放在一个虚拟网卡上,虚拟网卡收到数据包后转发给这台EC2的eth0。由于目的IP为172.31.47.15,属于LOCAL类型,所以命中nat的PREROUTING的第3条规则,进入DOCKER链:
Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
5 420 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
90 4692 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
21 1120 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8081 to:172.17.0.3:80
在Docker链中,由于我们的数据包是发往8080端口的,且in网卡是eth0不是docker0,所以命中第二条规则,被DNAT修改目标地址和端口为172.17.0.2:80,也就是container A的地址和端口。
经过了DNAT之后,根据目的IP地址172.17.0.2,查询路由表发现,数据包需要发往通过docker0网卡发往container A。
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.32.1 0.0.0.0 UG 100 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.31.32.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
172.31.32.1 0.0.0.0 255.255.255.255 UH 100 0 0 eth0
此外,由于数据包不是发给本机的,需要路由,所以数据包在发往container A之前还要经过FORWARD链与POSTROUTING链的检查。接下来的部分与前文相似,感兴趣的朋友可以继续分析。
总结
从上面的分析,我们可以看到,docker的bridge模式下:
- container的网络被linux的network namespace隔离;
- 不同的container之间通过veth pair和docker0 bridge实现连通;
- container与外网之间的连接需要通过NAT。