简介
通过 Docker 容器可以实现文件系统,网络和内核的隔离。 Docker 网络是使用 Docker 的一个很重要的知识点.。在不了解 Docker 网络的情况下使用 Docker 部署应用可能会出现 Docker 容器跨过宿主机防火墙(iptables)的限制直接与取得外网访问权的情况.。在这篇文章中将会分析安装 Docker 对宿主机网络设备和 iptables 两个重要的网络环境的影响。并会分析何为 Docker 容器网络的隔离性, 如何通过控制宿主机 iptables 来控制 Docker 容器的网络访问权限。
跨越宿主机防火墙示例
首先在宿主机上设置一个防火墙,将 INPUT 链的默认策略设为 DROP
$ iptables -P INPUT DROP
然后执行查看 iptables 的命令
$ iptables-save
可以看到 filter 表的 INPUT 链的默认策略为 DROP
INPUT 默认策略设为 DROP
然后 ping 百度, 由于 INPUT 链的默认规则为 DROP ,导致 DNS 服务器返回的解析结果不能正确接受, 所以这里直接 ping 解析出的 ip 地址。
$ ping -c4 14.215.177.39
执行结果为
PING 百度 IP
可以看到宿主机在不使用 iptables 手动设置 INPUT 链的情况下已经和外界网络隔绝了。
接着创建一个 Docker 容器并执行 ping -c4 www.baidu.com
的命令
$ docker run --rm debian:stretch-slim bash -c "
echo 'deb http://mirrors.163.com/debian/ stretch main non-free contrib' > /etc/apt/sources.list
&& apt-get update > /dev/null
&& apt-get install -y inetutils-ping >/dev/null 2>&1
&& ping -c4 www.baidu.com"
容器中 PING 百度
可以看到能够正确接收 DNS 服务器返回的解析结果,,并且能够成功 ping 通,
这就是所谓的 Docker 容器网络环境和宿主机网络环境互相隔离。接下来就会详解 Docker 是如何操作宿主机的网络设备来实现 Docker 容器和宿主机网络隔离的。
安装 Docker 对宿主机网络环境的影响
在这里需要准备一个全新安装的虚拟机(这里使用的是 debian9 gnome), 然后对比安装前后宿主机网络环境的变化。
安装前网络环境
网络设备
首先查看安装 Docker 前的网络设备, 输入查看网络设备的命令
$ ip a
初始网络设备
只有两个网络设备, 一个是主机的回环地址, 一个是主机的网卡。
iptables
接着查看安装 Docker 前的 iptables,输入查看 iptables 的命令
$ iptables-save
然后输出的内容可能为空,也可能是下面的内容
# Generated by iptables-save v1.6.0 on Sat Mar 16 00:29:12 2019
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
# Completed on Sat Mar 16 00:29:12 2019
无论是空还是上述内容,都是代表着 iptables 没有经过修改。
路由表
最后查看安装 Docker 前的路由表,输入查看路由表的命令
$ ip r
输出的内容只有简单的两行
安装后网络环境
这里使用的 Docker 版本是 Docker version 18.09.3
网络设备
使用 ip a
命令查看网络设备
安装 Docker 后的网络设备
可以看到安装后, 网络设备中增加了 docker0 设备。这个新增网络设备的作用将会在后面说到.
iptables
接着还是使用 iptables-save
命令查看 iptables.。显示如下
# Generated by iptables-save v1.6.0 on Sat Mar 16 00:41:26 2019
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [4:237]
:POSTROUTING ACCEPT [4:237]
: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 DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Sat Mar 16 00:41:26 2019
# Generated by iptables-save v1.6.0 on Sat Mar 16 00:41:26 2019
*filter
:INPUT ACCEPT [7:704]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [8:669]
:DOCKER - [0:0]
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
COMMIT
# Completed on Sat Mar 16 00:41:26 2019
可以看到安装 Docker 对 iptables 的 NAT 表和 FILTER 表都作了较大的改动。
NAT 表改动的解析如下:
# DOCKER 链
:DOCKER - [0:0]
# 如果请求的目标地址是本机的地址, 那么将请求转到 DOCKER 链处理
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
# 如果请求的目标地址不匹配 127.0.0.0/8, 并且目标地址属于本机地址, 那么将请求跳转到 DOCKER 链处理
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
# 对于来自于 172.17.0.0/16 的请求, 目标地址不是 docker0 所在的网段的地址, POSTROUTING 链将会将该请求伪装成宿主机的请求转发到外网
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
# 由 docker0 设备传入的请求 DOCKER 链会返回上一层处理
-A DOCKER -i docker0 -j RETURN
使用netfilter/iptables时经常能在匹配规则中看到-m addrtype --dst-type这样的内容,何解
FILTER 表改动的解析如下:
# DOCKER 链
:DOCKER - [0:0]
# DOCKER-ISOLATION-STAGE-1 链
:DOCKER-ISOLATION-STAGE-1 - [0:0]
# DOCKER-ISOLATION-STAGE-2 链
:DOCKER-ISOLATION-STAGE-2 - [0:0]
# DOCKER-USER 链
:DOCKER-USER - [0:0]
# FORWARD 链的请求跳转到 DOCKER-USER 链处理
-A FORWARD -j DOCKER-USER
# FORWARD 链的请求跳转到 DOCKER-ISOLATION-STAGE-1 链处理
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
# FORWARD 链的请求如果目标是 docker0 所在的网段, 而且已经建立的连接或者和已建立连接相关那么接受请求
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# FORWARD 链请求目标是 docker0 所在的网段, 那么跳转到 DOCKER 链处理
-A FORWARD -o docker0 -j DOCKER
# FORWARD 链的请求来自于 docker0 所在网段, 而且目标网段不是 docker0 所在网段, 那么接收请求.
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
# FORWARD 链的请求来自于 docker0 所在网段, 而且目标网段也是 docker0 所在网段, 那么接收请求
-A FORWARD -i docker0 -o docker0 -j ACCEPT
# DOCKER-ISOLATION-STAGE-1 链的请求如果来自 docker0 所在网段, 而且目标网段不属于 docker0 所在网段, 那么跳转到 DOCKER-ISOLATION-STAGE-2 处理
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
# DOCKER-ISOLATION-STAGE-1 链未处理的请求返回到上一层继续处理
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
# DOCKER-ISOLATION-STAGE-2 链的请求如果目标的网段为 docker0 所在网段则丢弃请求
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
# DOCKER-ISOLATION-STAGE-2 链未处理的请求返回到上一层继续处理
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
# DOCKER-USER 链未处理的请求返回到上一层继续处理
-A DOCKER-USER -j RETURN
一般情况下 iptables 处理请求的流程如下
默认 iptables 处理流程
iptables详解(1): iptables概念
安装 Docker 后 Docker 添加了 DOCKER, DOCKER-USER, DOCKER-ISOLATION-STAGE-1, DOCKER-ISOLATION-STAGE-2 四条链,按照初始化创建的 iptables 可以得出现在处理请求的流程如下
安装 Docker 后 iptables 的处理流程
iptables 的处理流程明显变复杂了。主要是 PREROUTING 链, FORWARD 链和 OUTPUT 链后都追加了 DOCKER 链。
路由表
再次执行 ip r`命令, 查看路由表
可以看到增加了将目标地址符合 172.17.0.0/16 的数据包转发到 docker0 网桥的路由项。
详析 Docker 如何跨越主机 iptables
首先执行以下命令:
$ docker run --rm debian:stretch-slim bash -c "
echo 'deb http://mirrors.163.com/debian/ stretch main non-free contrib' > /etc/apt/sources.list
&& apt-get update > /dev/null
&& apt-get install -y net-tools>/dev/null 2>&1
&& ifconfig"
创建一个容器, 并查看容器的网络设备信息
Docker 容器网络设备信息
可以看到容器内的 eth0 网卡的 ip 地址信息为 172.17.0.2/16 和宿主机的 docker0 ip 地址 172.17.0.1/16 处于一个网段下。当容器向外网发起请求的时候,首先会将强求发往网关,毫无疑问, 这个网关就是 docker0 网卡。容器的请求通过 docker0 网卡进入到宿主机中,按照上面的 iptables 的处理流程图解析容器 ping http://www.baidu.com 的过程。
由于 DOCKER-USER,DOCKER-ISOLATION-STAGE-1,DOCKER-ISOLATION-STAGE-2 链没有自定义规则所以这里解析一般会跳过这几条链
- 请求进入 PREROUTING 链
- http://www.baidu.com 的目标 ip 地址不属于宿主机的 ip 地址, 进入 FORWARD 链处理
- FORWARD 链的默认规则为抛弃,但是根据规则
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
由 docker0 接收的请求允许通过, 进入到 POSTROUTING 链。 - 根据 POSTROUTING 链的规则
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
,对源地址符合 172.17.0.0/16 模式的请求伪装成宿主机的请求发往外网。 - 外网响应通过宿主机网卡进入到 PREROUTING 链处理, 目标地址为 172.17.0.2/16。
- 172.17.0.2/16 不属于宿主机的 ip 地址, 进入 FORWARD 链处理
- 172.17.0.2/16 的网关是 docker0, 所以目标输出是 docker0, 根据 FORWARD 链的规则
-A FORWARD -o docker0 -j DOCKER
,输出到 docker0 的请求会进入到 DOCKER 链处理。 - DOCKER 链自定义规则为空, 返回到 FORWARD 继续处理,最终进入到 POSTROUTING.
- POSTROUTING 处理后最终从 docker0 输出进入到容器内部。
很明显, 容器的请求没有经过宿主机的 INPUT 链,所以在宿主机的 INPUT 链上做规则是没法限制容器的网络访问的. 需要限制容器网络访问应该对 DOCKER 链动手脚。
接下来我们对这个结论进行验证:
输入下面命令将 DOCKER 链对源地址为 14.215.177.38(百度某个服务器的IP) 的数据包的策略改为 DROP
$ iptables -A DOCKER -s 14.215.177.38 -j DROP
接着输入下面命令,创建容器并向 14.215.177.38 发出 ping 请求。
$ docker run --rm debian:stretch-slim bash -c "
echo 'deb http://mirrors.163.com/debian/ stretch main non-free contrib' > /etc/apt/sources.list
&& apt-get update > /dev/null
&& apt-get install -y inetutils-ping >/dev/null 2>&1
&& ping -c4 14.215.177.38"
结果是:
修改 Docker 链后容器内 PING 百度 IP
然而并没有像分析的那样 ping 请求被阻止。也就是说 FORWARD 链上的请求还没有跳转到 DOCKER 链就已经被接受了。观察 iptables 的规则, 应该是 -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
这条规则导致请求在进入 DOCKER 链之前就已经被接受。这个规则是很模糊的,如果数据包来自于已经建立连接的双方或者和已建立的连接有关则接受。现在删除这条规则,然后在 DOCKER 链的末尾增加通过所有请求的规则, 结合之前添加了的阻止来自 14.215.177.38 的数据包,可以达到只丢弃来自 14.215.177.38 的数据包的目标,依次执行命令
$ iptables -D FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
$ iptables -A DOCKER -j ACCEPT
然后,再次执行
$ docker run --rm debian:stretch-slim bash -c "
echo 'deb http://mirrors.163.com/debian/ stretch main non-free contrib' > /etc/apt/sources.list
&& apt-get update > /dev/null
&& apt-get install -y inetutils-ping >/dev/null 2>&1
&& ping -c4 14.215.177.38"
现在执行的结果是
再次修改 DOCKER 链后容器内 PING 百度 IP
这个结果可以印证,除了源于 14.215.177.38 的数据包外,其他的数据包都被接受了。原因有三
- http://mirrors.163.com 域名能够正确解析,说明和 DNS 服务器之间的通信没有被阻止
- 可以在 http://mirrors.163.com 中正确安装 inetutils-ping 说明和 http://mirrors.163.com 之间的通信没有被阻止
- 不能正确向 14.215.177.38 发出 ping 操作, 说明 14.215.177.38 的通信被阻止了
总结
通过上面的分析,可以得出,所谓的 Docker 网络的隔离性只在 INPUT 链,OUTPUT 链中体现。修改 PREROUTING 链,FORWARD 链,POSTROUTING 链都会影响到 Docker 容器的网络环境。要通过 DOCKER 链控制 Docker 容器的访问权限,需要先删除 -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
这条规则, 因为这条规则的不确定性太大。然后单纯通过 DOCKER 链来控制 Docker 容器的网络访问权限.
整篇文章对 Docker 网络的操作归根结底是对 iptables 的操作,要看懂整篇文章必须要对 Linux 的 iptables 有基本的认识, 会使用 iptables 基本的操作。
下一篇文章将会分析 Docker overlay 网络如何实现跨主机间的容器通信。
参考资料
- iptables(8) - Linux man page
- iptables详解(1): iptables概念
- 使用netfilter/iptables时经常能在匹配规则中看到-m addrtype --dst-type这样的内容,何解