docker的网络管理

docker 利用 linux 内核提供的 namespace 资源隔离来实现虚拟化,资源隔离包括很多方面,其中一项就是网络隔离(Network namespace),让运行在 docker 内的进程彷佛置身于一个独立的网络环境。因为网络隔离,就要解决 docker 容器和宿主机(运行 docker 的服务器)之间的通信问题。

veth

veth 是虚拟网络设备,因为其成对出现,所以也叫 veth-pair。它的特点是一头接收到了数据,就会自动发送到另外一头,就像是一根网线一样,所以它被 docker 用来连接两个宿主机和 docker 之间的网络 namespace,这样就实现了不同 namespace 之间互相通信。

先跑一个 busybox,busybox 有比较全的功能。可以先 pull 一个 busybox 镜像然后运行,也可以直接运行,docker 会自动寻找 busybox 镜像。

1
2
docker pull busybox
docker run -it --rm --name busybox busybox

然后另外开一个终端观察宿主机上的网卡情况,会发现当一个 docker 运行后,宿主机上就会多出一个以 “veth” 开头的虚拟设备。例如我这里有个 “veth4c90e69”,使用 ethtool 命令查看它的另一头:

1
2
3
$ ethtool -S veth4c90e69
NIC statistics:
peer_ifindex: 7

这是只显示它另一头的设备编号是 7,那么这个另一头到底在哪儿呢?回到 busybox,查看一下 ip 情况:

1
2
3
4
5
6
7
8
# ip addr show
1: lo <LOOKBACK,UP,LOWER_UP> mtu 65536...
......

7: eth0:if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500...
......
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
......

这下清楚了,eth0 网卡前面的编号就是 7,所以说 veth 一头在宿主机,一头在 busybox 这个 docker 里,把两个隔离的网络连在一起。当然仔细的朋友也发现了,eth0 后面接的 if8 不就说的是它对应的是编号 8 的网络接口嘛,去宿主机查看 veth4c90e69,它的编号就是 8。

docker0 网桥

除了上面的 vnet-pair,docker 还会自动创建一个名为 docker0 的网桥。网桥字面意思网段的桥梁,他可以实现加入到它的网卡设备进行通信。使用 brctl 命令查看一下 docker0 里加入了那些设备。

1
2
3
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242ede3ad75 no veth4c90e69

是不是很眼熟,veth4c90e69 设备就是上面 veth-pair 放在宿主机上的一头。所以到此就知道了,docker 先创建一个名为 docker0 的网桥,当 busybox 运行时,docker 会为其拉出来一根 veth-pair “网线”,连接到 docker0 网桥;又因为 docker0 网桥在宿主机里,自然就实现了 busybox 容器和宿主机之间的通信。

交换机是类似于网桥的物理设备,差别就是网桥只有两端口,交换机有多个端口。网桥和交换机一样处在网络模型的第二层:数据链路层,只能解析数据帧的 MAC 地址,所以网桥应该没有 ip 地址才对,但事实上 docker0 会被默认分配一个 172.17.0.1。原因是 docker0 网桥在这里兼职为 busybox 的默认网关,网关处在网络模型的第三层:传输层,就要有一个 ip 地址。去 docker 实例查看它的路由表:

1
2
3
4
# route -n
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.17.0.1,就是 docker0 网桥嘛。

docker 流量怎么出来

上面知道了 busybox 的 ip 是 172.17.0.2,如果它要访问 Internet,比如要 ping 一下 bing.com 域名,根据它的路由表走向,我们知道它会找到 172.17.0.1,也就是 docker0,然后借助宿主机正常出去。但是你这个出去的数据包里的源地址是 172.17.0.2 呀,bing.com 后面要响应你,但是面对一个内网 ip 也只能迷茫。。。

docker 怎么解决这个问题呢?docker 会借助宿主机上的 iptables,使用源地址转换(SNAT)将原本源地址是内网的 ip 转换成宿主机上的公网 ip,bing.com 响应后根据源地址将数据包回传给宿主机,宿主机上的 iptables 查找记录的 SNAT 转换情况,发现这个数据包是给 172.17.0.2 的,也就回传到了 docker 里。

1
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

流量怎么进入 docker

busybox 有一个 httpd 服务,我现在让 httpd 监听 80 端口。虽然 bosybox 的 ip 是 172.17.0.2,但是外面怎么可能访问到这个内网 ip,就只能通过宿主机来间接访问 busybox 的 80 端口了。为了便于区别,这个使用宿主机的 8080 端口访问 busybox 的 80 端口。还是通过 iptables,使用目标地址转换(DNAT)将访问宿主机 8080 端口的流量全都转发都 172.17.0.2 的 80 端口上去。

1
iptables -t nat -A PREROUTING ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80

busybox 里的 httpd 要响应请求,会先根据路由表到 172.17.0.1 也是 docker0,然后根据刚才的 DNAT 记录把源地址修改为宿主机 ip。这就让外面的误以为就是宿主机在提供 httpd 服务,因为这些操作都是在宿主机内部完成的,对外不可见。

幸运的是我们不需要自己手动去创建 8080 到 80 的映射,只需要退出 busybox,使用一个 - p 参数标明映射关系,docker 会自动实现 iptables 的 DNAT。

1
docker run -it --p 8080:80 --rm --name busybox busybox

更幸运的是,上面的一切 docker 都会自动帮你实现好了。但是了解 docker 的网络管理也不是坏事。

其他网络驱动

docker 默认使用 bridge 也就是网桥,默认的网桥名为 docker0。除了 bridge,还有 host、container、none。

host 不创建 Network namespace,而是跟宿主机共用一个 Network namespace。在 docker run 的时候指定参数:–net=host 即可,在这个 docker 里你会看到和宿主机同样的网卡信息和路由表等,宿主机占用的端口 docker 就不能再使用了。除了共用一个网络,其他都是独立的。

container 也是不创建 Network namespace,但是跟已存在的 docker 共用一个 Network namespace,情况跟 host 一样。在 docker run 的时候指定参数:net=container:busybox。

none 的话会创建 Network namespace,但是 ip、路由什么都为空,需要自己配置。