Docker能为我们提供很强大和灵活的网络能力,很大程度上要归功于与iptables的结合。在使用时,你可能没有太关注到 iptables在其中产生的作用,这是因为Docker已经帮我们自动的完成了相关的配置。
iptables在Docker中的应用主要是用于网络流量控制和安全控制。可以使用iptables规则来限制Docker容器的网络访问,以及将外部流量重定向到Docker容器。
docker的daemon进程有个--iptables的参数,可以使用它来控制是否要自动启用iptables规则的,默认已经设置成了开启(true)。所以通常我们不会过于关注到它的工作。
$ dockerd --help |grep iptables --iptables Enable addition of iptables rules (default true)本文中,为了避免环境的干扰,我将使用DinD(docker in docker)的环境来进行介绍,可通过如下方式启动该环境:
$ sudo docker run --rm -d --privileged docker:dind 关闭Docker的iptables支持在启动一个Docker daemon时关闭iptables支持,将--iptables参数设置为false。
$ sudo docker run --rm -d --privileged docker:dind dockerd --iptables=false f43d990164ef66401f7424364bcf85e6506e2f1419f146b940c2cd01a6463485 $ sudo docker container exec -it f4 iptables -t filter -nvL --line-numbers Chain INPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination $ sudo docker container exec -it f4 iptables -t nat -nvL --line-numbers Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain INPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination可以看到,当docker的daemon加了--iptables=false的参数时,默认没有任何规则的输出。
可以使用iptables-save命令将iptables的规则转储到stdout中,更方便查看:
$ iptables-save *mangle :PREROUTING ACCEPT [15992:2680378] :INPUT ACCEPT [3831:436980] :FORWARD ACCEPT [576:134155] :OUTPUT ACCEPT [2486:235970] :POSTROUTING ACCEPT [2931:348259] COMMIT *raw :PREROUTING ACCEPT [6:348] :OUTPUT ACCEPT [4:312] COMMIT *filter :INPUT ACCEPT [82:4720] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [49:4028] COMMIT *nat :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] COMMIT 开启Docker的iptables支持在启动一个Docker的daemon时不指定--iptables选项,因为默认就是true。
$ sudo docker run --rm -d --privileged docker:dind dockerd 5a236c0f17f58e71a4ac7a073b224b1d26f29daea07508a383f0dc561cf0644f $ sudo docker container exec -it 5a iptables -t nat -nvL --line-numbers Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination 1 0 0 DOCKER 0 -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination 1 0 0 DOCKER 0 -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination 1 0 0 MASQUERADE 0 -- * !docker0 172.18.0.0/16 0.0.0.0/0 Chain DOCKER (2 references) num pkts bytes target prot opt in out source destination 1 0 0 RETURN 0 -- docker0 * 0.0.0.0/0 0.0.0.0/0 $ sudo docker container exec -it 5a iptables -t filter -nvL --line-numbers Chain INPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination 1 0 0 DOCKER-USER 0 -- * * 0.0.0.0/0 0.0.0.0/0 2 0 0 DOCKER-ISOLATION-STAGE-1 0 -- * * 0.0.0.0/0 0.0.0.0/0 3 0 0 ACCEPT 0 -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED 4 0 0 DOCKER 0 -- * docker0 0.0.0.0/0 0.0.0.0/0 5 0 0 ACCEPT 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 6 0 0 ACCEPT 0 -- docker0 docker0 0.0.0.0/0 0.0.0.0/0 Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination Chain DOCKER (1 references) num pkts bytes target prot opt in out source destination Chain DOCKER-ISOLATION-STAGE-1 (1 references) num pkts bytes target prot opt in out source destination 1 0 0 DOCKER-ISOLATION-STAGE-2 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 2 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0 Chain DOCKER-ISOLATION-STAGE-2 (1 references) num pkts bytes target prot opt in out source destination 1 0 0 DROP 0 -- * docker0 0.0.0.0/0 0.0.0.0/0 2 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0 Chain DOCKER-USER (1 references) num pkts bytes target prot opt in out source destination 1 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0可以看到,它比刚才关闭iptables支持时多了几条链:
DOCKERDOCKER-ISOLATION-STAGE-1DOCKER-ISOLATION-STAGE-2DOCKER-USER以及增加了一些转发规则,以下将具体介绍。
DOCKER-USER链在上述新增的几条链中,我们先来看最先生效的DOCKER-USER 。
filter表中FORWARD链的第一条规则是:
-A FORWARD -j DOCKER-USER这表示流量进入FORWARD链后,所有的流量直接进入到DOCKER-USER链。
而filter表中DOCKER-USER链中的规则是:
-A DOCKER-USER -j RETURN这表示流量进入DOCKER-USER链处理后,(如果无其他处理)可以再RETURN回原先的链,进行后续规则的匹配。
这其实是Docker预留的一个链,供用户来自行配置的一些额外的规则的。
Docker默认的路由规则是允许所有客户端访问的,如果你的Docker服务运行在公网,或者你希望避免Docker中容器被局域网内的其他客户端访问,那么你需要在这里添加一条规则。
比如, 你仅仅允许192.168.1.100访问,但是要拒绝其他客户端访问:
$ sudo iptables -A DOCKER-USER -i eth0 ! -s 192.168.1.100 -j DROP这个规则表示将源IP不是192.168.1.100的流量全部丢失。
此外,Docker在重启之类的操作时候,会进行iptables相关规则的清理和重建,但是DOCKER-USER链中的规则可以持久化,不受影响。
DOCKER-ISOLATION-STAGE-1/2链filter表中的DOCKER-ISOLATION-STAGE-1和DOCKER-ISOLATION-STAGE-2这两条链作用类似,这里一起进行介绍。
先来看一个例子:
// 创建一个自定义网络mynetwork $ sudo docker network create mynetwork // box1容器运行在默认的网络下 $ sudo docker container run --rm -d --name box1 busybox /bin/sh -c"while true; do sleep 3600; done"// box2和box3运行在自定义的网络mynetwork下 $ sudo docker container run --rm -d --name box2 --network mynetwork busybox /bin/sh -c"while true; do sleep 3600; done"$ sudo docker container run --rm -d --name box3 --network mynetwork busybox /bin/sh -c"while true; do sleep 3600; done"部署示意图如下:
在box2中访问box3,可以访问:
$ sudo docker container exec -it box2 ping 172.19.0.3 -c 3 PING 172.19.0.3 (172.19.0.3): 56 data bytes 64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.077 ms 64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.301 ms 64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.196 ms --- 172.19.0.3 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 0.077/0.191/0.301 ms在box2中访问box1,不可以访问:
$ sudo docker container exec -it box2 ping 172.17.0.2 -c 3 PING 172.17.0.2 (172.17.0.2): 56 data bytes --- 172.17.0.2 ping statistics --- 3 packets transmitted, 0 packets received, 100% packet loss可以看到,如果是相同network的容器是可以ping成功的,但如果是不同network的容器则不能ping通。
那么两个network之间不能通信是怎么进行隔离的呢?这就是filter表中的DOCKER-ISOLATION-STAGE-1和DOCKER-ISOLATION-STAGE-2这两条链的功劳。
当我们创建一个新的network时,系统层面会创建一个新的bridge端口br-d4bf6d77f3cf,再加上默认创建的bridge端口docker0,系统的bridge接口列表如下:
$ ip addr show type bridge 3: docker0:流量接着会进入filter表FORWARD链中的第二条规则:
-A FORWARD -j DOCKER-ISOLATION-STAGE-1这表示流量进入FORWARD链后,所有的流量直接进入到DOCKER-ISOLATION-STAGE-1链。
再来看filter表中DOCKER-ISOLATION-STAGE-1链中的规则:
-A DOCKER-ISOLATION-STAGE-1 -i br-d4bf6d77f3cf ! -o br-d4bf6d77f3cf -j DOCKER-ISOLATION-STAGE-2 -A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2 -A DOCKER-ISOLATION-STAGE-1 -j RETURN第一条规则表示从接口br-d4bf6d77f3cf进入但是不从接口br-d4bf6d77f3cf出去的包会进入DOCKER-ISOLATION-STAGE-2链。
第二条规则和第一条规则类似,表示从接口docker0进入但是不从接口docker0出去的包会进入DOCKER-ISOLATION-STAGE-2链。
第三条规则表示再RETURN回原先的链,进行后续规则的匹配,进入这条规则可能有三种情况:
接下来看filter表中DOCKER-ISOLATION-STAGE-2链中的规则:
-A DOCKER-ISOLATION-STAGE-2 -o br-d4bf6d77f3cf -j DROP -A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP -A DOCKER-ISOLATION-STAGE-2 -j RETURN第一条规则表示从接口br-d4bf6d77f3cf出去的包直接丢弃,结合DOCKER-ISOLATION-STAGE-1链来看,就是让从接口docker0进入且从接口br-d4bf6d77f3cf出去的包的直接丢弃,使得docker0网络中的容器无法访问my-network网络中的容器。
第二条规则与第一条规则类似,表示从接口docker0出去的包直接丢弃,结合DOCKER-ISOLATION-STAGE-1链来看,就是让从接口br-d4bf6d77f3cf进入且从接口docker0出去的包的直接丢弃,使得br-d4bf6d77f3cf网络中的容器无法访问docker0网络中的容器。
第三条规则表示再RETURN回原先的链,进行后续规则的匹配,进入这条规则说明是docker内的容器访问外部网络(非Docker中的网络)。
Docker通过这两条链分为两个阶段对桥接网络进行了隔离,使得各个桥接网络直接无法通讯。
看到这里,你可能会问为什么要分两个阶段进行隔离?用一条链直接隔离行不行?
答案是行,一条链也能隔离,Docker很早的版本就是这样做的。
但是当时的实在超过30个network以后,就会导致Docker启动很慢。所以后来做了这个优化,将这部分的复杂度从O(N^2)降低到O(2N),Docker就不再会出现启动慢的情况了。
DOCKER链最后我们来看看DOCKER链,这是Docker中使用最为频繁的一个链,也是规则最多的链,但它却很好理解。通常情况下,如果不小心删掉了这个链的内容,可能会导致容器的网络出现问题,手动修复下,或者重启Docker均可解决。
filter表和nat表中都存在DOCKER链。
什么样的包会进入到filter表的DOCKER链呢?
-A FORWARD -o br-d4bf6d77f3cf -j DOCKER -A FORWARD -o docker0 -j DOCKERFORWARD链中请求目标接口是docker0、br-d4bf6d77f3cf, 那么跳转到DOCKER链处理,也就是包转发到docker创建的bridge接口时就会进入DOCKER链。
什么样的包会进入到nat表的DOCKER链呢?
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER第一条规则表示如果包进入时请求的目标地址是本机的地址, 那么将请求转到DOCKER链处理。
第二条规则表示如果包出去时请求的目标地址不匹配127.0.0.0/8, 并且目标地址属于本机地址, 那么将请求跳转到 DOCKER链处理。
这里我们启动一个容器,并进行端口映射,来看看会有哪些变化。
$ sudo docker container run --rm -d --name httpd -p 8080:80 httpd之后再次执行iptables-save,对比当前的结果与上次的差别:
filter表中的DOCKER链增加如下一条规则:
-A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT这条规则表示对目标地址是172.17.0.3/32且不是从docker0进入的但从docker0出去的,目标端口是80的TCP协议则接收。
nat表中增加如下两条规则:
-A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 80 -j MASQUERADE -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.3:80第一条规则表示为172.17.0.3上目标端口为80的流量执行MASQUERADE(SNAT)动作;
第二条规则表示在自定义的DOCKER链中,如果入口不是docker0并且目标端口是8080则进行DNAT动作,将目标地址转换为172.18.0.3:80。简单点来说,这条规则就是为我们提供了Docker容器端口转发的能力,将访问主机本地8080端口流量的目标地址转换为172.18.0.3:80。