烦恼一般都是想太多了。

0%

Docker的网络结构

Docker 支持几种网络模式,不过默认使用的是 bridge 模式,完整的介绍可以在 https://docs.docker.com/network/ 看到。我这寻思要往其内部的细节上来看一下。

docker network cli

我们可以用命令

docker network ls 
NETWORK ID NAME DRIVER SCOPE
2efb3f41ed5a bridge bridge local
0791a289b02c host host local
4d8db1d88c60 none null local

来查看 docker 维护的网络列表。

可以用

docker network inspect 2efb3f41ed5a

来查看某个网络的详细信息

[
{
"Name": "bridge",
"Id": "2efb3f41ed5a4dced8880c92bf5ac5e0d5764391e796666f24be87f56d16cca9",
"Created": "2019-11-27T08:53:19.457732706Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"69b2aa0eba4d153a41f657a5cd85c1abea4ce91bfff906bb9d1566017377652f": {
"Name": "condescending_lewin",
"EndpointID": "32851abcca4597d40fe272fc357555ff54caf5bb25068be83475bb449708c367",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]

从中可以看到,我们的 bridge 网络还配置了IP 信息,包括子网,路由信息。

同时也列出了加入此网络的容器信息。这里我们看到的是 172.17.0.2/16。

我这里看到了配置的信息,但我们可以进入宿主机,来查看更详细的内容。

Host Intefaces

网桥信息。

brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024291f343bc no vethf9c5f16

docker0 是一个网桥,同时只有接口 vethf9c5f16 加入了这个网桥中。虚拟一台网接口 vethf9c5f16 会有一个对端,这两个端点间收到的任何数据都会立刻传输到另外一端去。具体的信息我们可以看 这里 veth(4)

整个网络结构如下。

+------------------------------------------------------------------------+                                
| |
| |
| |
| +--------------------------------------------------------------------+ |
| | | |
| | TCP/IP Stack | |
| | | |
| +--------------------------------------------------------------------+ |
| | | |
| | | |
| | | |
| | | |
| +-----------+ +-----------+ +---------------------------+ |
| | | | | | Ubuntu | | +
| | eth0 | | docker0 | | | |
| | | | | | | |
| +-----------+ +-----------+ | | |
| | | | |
| | | | |
| +-----------+ | | |
| | | | +-----------+ | |
| |vethf9c5f16|-----------------| eth0 | | |
| | | | + ---------+ | |
| +-----------+ | | |
| +---------------------------+ |
| |
+------------------------------------------------------------------------+

网卡信息

在宿主机上执行,查看网卡列表

ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 brd 127.255.255.255 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 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff
inet 192.168.65.3/24 brd 192.168.65.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::50:ff:fe00:1/64 scope link
valid_lft forever preferred_lft forever
3: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
link/ipip 0.0.0.0 brd 0.0.0.0
4: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1
link/tunnel6 :: brd ::
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:91:f3:43:bc 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:91ff:fef3:43bc/64 scope link
valid_lft forever preferred_lft forever
7: vethf9c5f16@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether ce:d6:05:9f:b9:47 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::ccd6:5ff:fe9f:b947/64 scope link
valid_lft forever preferred_lft forever

Docker0 vethf9c5f16@if6 都配置了 IP 信息,所以实际上是并不止是一个网桥,而且充当了路由的功能。我们可以从 Container 上来验证这一点:

ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2

默认路由是 172.17.0.1 而对于同一子网内的,则是直接通过内核交换。

这里,在 TCP/IP 的协议栈内,需要将从网卡拿到的数据,和要发出的数据正确的发送,必须得开启 ip 转发,和相应的 iptables 规则,不然是不能进行数据的转发的。

数据流向

通过上面的网络图,我们可以看出数据是如何流向的。

From Container

对于从 Container 发出的数据会经历:

ubuntu:eth0 -> vethf9c5f16 -> docker0 -> tcp/ip stack -> eth0 -> internet

数据从 docker0 到 tcp/ip stack 后,会进行 snat 转换,然后再从 网卡发送出去。我们先来看一下我们的环境:

eno2 192.168.50.201
docker0 172.17.0.1/16
ubuntu:eht0 172.17.0.2/16

现在我们在 Ubuntu 上 ping www.baidu.com 然后在 Host 上抓包看看情况:

tcpdump -i eno2 icmp  -n
15:07:01.923115 IP 104.193.88.123 > 192.168.50.201: ICMP echo reply, id 2640, seq 337, length 64
15:07:02.727196 IP 192.168.50.201 > 104.193.88.123: ICMP echo request, id 2640, seq 338, length 64
15:07:02.923899 IP 104.193.88.123 > 192.168.50.201: ICMP echo reply, id 2640, seq 338, length 64
15:07:03.728925 IP 192.168.50.201 > 104.193.88.123: ICMP echo request, id 2640, seq 339, length 64
15:07:03.925620 IP 104.193.88.123 > 192.168.50.201: ICMP echo reply, id 2640, seq 339, length 64

看到连接对和我们 Container 不相关。我们再看一下 docker0 上的数据包:

tcpdump -i docker0 icmp -n
15:08:14.811120 IP 172.17.0.2 > 104.193.88.123: ICMP echo request, id 2640, seq 410, length 64
15:08:15.008025 IP 104.193.88.123 > 172.17.0.2: ICMP echo reply, id 2640, seq 410, length 64
15:08:15.811030 IP 172.17.0.2 > 104.193.88.123: ICMP echo request, id 2640, seq 411, length 64
15:08:16.008168 IP 104.193.88.123 > 172.17.0.2: ICMP echo reply, id 2640, seq 411, length 64
15:08:16.811154 IP 172.17.0.2 > 104.193.88.123: ICMP echo request, id 2640, seq 412, length 64
15:08:17.008481 IP 104.193.88.123 > 172.17.0.2: ICMP echo reply, id 2640, seq 412, length 64
15:08:17.811467 IP 172.17.0.2 > 104.193.88.123: ICMP echo request, id 2640, seq 413, length 64
15:08:18.008269 IP 104.193.88.123 > 172.17.0.2: ICMP echo reply, id 2640, seq 413, length 64

可以看到,数据包的在出去的时候,被做了 SNAT,入访的时候,做了 DNAT。我们还可以看一下跟踪链信息:

contrack 工具查看跟踪信息

conntrack -L -n # -n 表示进行了 snat
icmp 1 29 src=172.17.0.2 dst=104.193.88.123 type=8 code=0 id=2640 src=104.193.88.123 dst=192.168.50.201 type=0 code=0 id=2640 mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1

conntrack -L -g # -g 表示进项 dnat
icmp 1 29 src=172.17.0.2 dst=104.193.88.123 type=8 code=0 id=2640 src=104.193.88.123 dst=192.168.50.201 type=0 code=0 id=2640 mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1

所以就很明白了,数据的流向了。

而且 docker0 是使用了 IP 层进行通信,而不是第二层,这和名叫 bridge 这个设备有点迷惑人哈。

iptables

是不是上,进行数据转发这个,使用的是 iptables 来进行管理设置的,在开启了 net.ipv4.ip_forward = 1 的情况下只需要一就行了。

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

就是说,从 172.17.0.0/16 段(我们当前的 container 和 docker0 本身)发出的数据,目标网卡接口不是 docker0 的,进行一下 MASQUERADE 的意思。

实际上 MASQUERADE 就是进行 nat 的意思,其和下面一种写法是等价的:

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j SNAT --to dst-IP

不过:MASQUERADE 会自动选择一个 IP 地址。

容器间通信

默认情况下,我们建立的容器都会连接在 docker0 下面,互相之间可以通过 IP 进行通信,但是无法进行容器名进行通信。而如果我们自定义一个 bridge 网络就可以使用 容器名进行通信,自定义的 bridge 自带了容器名的解析功能。

docker network create -d bridge net # 新建网络
docker network disconnect bridge busybox # 离开默认 docker0 网络
docker network disconnect bridge ubuntu
docker network connect net busybox # 加入新建的 net 网络
docker network connect net ubuntu
docker exec busybox ping ubuntu # 容器名通信测试
PING ubuntu (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.126 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.110 ms