大家好,我是张锦涛。上周有小伙伴在群里问到Docker和Iptables的关系,就详细说说吧。Docker能够为我们提供非常强大和灵活的网络能力,很大程度上得益于与iptables的结合。在使用的时候大家可能不太注意iptables的作用,因为Docker已经自动帮我们完成了相关的配置。(MoeLove)?~dockerd--help|grepiptables--iptables启用添加iptables规则(默认true)dockerdaemon有一个--iptables参数,用于控制是否自动启用iptables规则,这个参数已经被设置默认变为打开状态(真)。所以通常我们不会过多关注它的工作。在本文中,为了避免环境干扰,我将使用docker环境下的docker进行介绍,启动方法如下:(萌萌哒)?~dockerrun--rm-d--privilegeddocker:dindf323aef7b532ba6d575ca6f9444a08f1a55f2447afipec2e853954694c034e6aetable0iptablesbasicsis用于配置Linux内核防火墙的工具,可用于检测、修改转发、重定向和丢弃IPv4数据包。它使用内核的ip_tables功能,因此需要Linux2.4+内核。同时为了方便管理,iptables根据不同的用途组织了多个表;每个表包含许多预定义的链;每个链包含顺序遍历的规则;这些规则定义动作匹配规则和目标。对于用户来说,我们通常需要与链和规则进行交互。有一张经典图片可以理解iptables的主要工作流程:图片来源:https://www.frozentux.net/ipt...上面小写字母是表,大写字母是链。IP数据包必须从上到下通过此图。引用自ArchWiki但这不是本文的重点,就不展开了。如果大家对iptables的内容感兴趣,欢迎留言,后面可以写一篇完整的文章。Dockernetwork和iptables接下来我们来看看Docker的iptables开启和关闭的具体区别。关闭Docker的iptables支持在本文的开头,我已经向大家介绍过dockerdaemon有一个--iptables参数来控制是否使用iptables。我们使用以下命令启动docker守护进程并关闭iptables支持。(MoeLove)?~dockerrun--rm-d--privilegeddocker:dinddockerd--iptables=false7135a54c913af5e9ce69a45a0819475503ea9e3c5c673d62d9d38f0f0896179d进入这个容器并查看它所有的iptables规则:(MoeLovedoc)#-savesh-exe由iptables-savev1.8.8于MonDec1201:46:382022*filter生成:INPUTACCEPT[0:0]:FORWARDACCEPT[0:0]:OUTPUTACCEPT[2:80]COMMIT#CompletedonMonDec1201:46:382022可以看到dockerdaemon加上--iptables=false参数时,默认没有任何规则的输出。启用Docker的iptables支持以使用以下命令启动docker守护进程,没有显式传递--iptables选项,因为默认值为true。(Moelove)?#由iptables-savev1.8.8于MonDec1214:48:162022*nat:PREROUTINGACCEPT[0:0]:INPUTACCEPT[0:0]:OUTPUTACCEPT[1:40]:POSTROUTINGACCEPT[1:40]:DOCKER-[0:0]-APREROUTING-maddrtype--dst-typeLOCAL-jDOCKER-AOUTPUT!-d127.0.0.0/8-maddrtype--dst-typeLOCAL-jDOCKER-APOSTROUTING-s172.18.0.0/16!-odocker0-jMASQUERADE-ADOCKER-idocker0-jRETURNCOMMIT#CompletedonMonDec1214:48:162022#由iptables-savev1.8.8生成于MonDec1214:48:162022*filter:INPUT接受[0:0]:转发接受[0:0]:输出接受[2:80]:DOCKER-[0:0]:DOCKER-ISOLATION-STAGE-1-[0:0]:DOCKER-ISOLATION-STAGE-2-[0:0]:DOCKER-USER-[0:0]-AFORWARD-jDOCKER-USER-AFORWARD-jDOCKER-ISOLATION-STAGE-1-AFORWARD-odocker0-mconntrack--ctstateRELATED,ESTABLISHED-jACCEPT-AFORWARD-odocker0-jDOCKER-AFORWARD-idocker0!-odocker0-jACCEPT-AFORWARD-idocker0-odocker0-jACCEPT-ADOCKER-ISOLATION-STAGE-1-idocker0!-odocker0-jDOCKER-ISOLATION-STAGE-2-ADOCKER-ISOLATION-STAGE-1-jRETURN-ADOCKER-ISOLATION-STAGE-2-odocker0-jDROP-ADOCKER-ISOLATION-STAGE-2-jRETURN-ADOCKER-USER-jRETURNCOMMIT#CompletedonMonDec1214:48:162022你可以看到它比刚刚关闭iptables支持时多了几个链:DOCKERDOCKER-ISOLATION-STAGE-1DOCKER-ISOLATION-STAGE-2DOCKER-USER和一些转发规则已添加。下面将详细介绍DOCKER-USER链。在上述新链中,我们先来看第一个生效的DOCKER-USER。*filter:DOCKER-USER-[0:0]-AFORWARD-jDOCKER-USER...-ADOCKER-USER-jRETURN以上规则在过滤表中有效:第一个是-AFORWARD-jDOCKER-USER表示流量进入FORWARD链后,直接进入DOCKER-USER链;最后一行-ADOCKER-USER-jRETURN意思是流量进入DOCKER-USER链进行处理后,(如果没有其他处理)可以返回原链,用于匹配后续规则。这其实是Docker预留的一条链,供用户自己配置一些额外的规则。Docker默认的路由规则是允许所有客户端访问。如果你的Docker运行在公网,或者你想防止Docker中的容器被局域网中的其他客户端访问,你需要在这里添加一条规则。例如,您只允许访问100.84.94.62,但拒绝访问其他客户端:iptables-IDOCKER-USER-i!-s100.84.94.62-jDROP另外,当Docker重启等操作时,iptables相关规则会被清理重建,但DOCKER-USER链中的规则可以持久化,不受影响。具体实现在docker/libnetwork下。下面是DOCKER-USER链的相关代码:constuserChain="DOCKER-USER"funcarrangeUserFilterRule(){ifctrl==nil||!ctrl.iptablesEnabled(){return}iptable:=iptables.GetIptable(iptables.IPv4)_,err:=iptable.NewChain(userChain,iptables.Filter,false)iferr!=nil{logrus.Warnf("Failedto创建%s链:%v",userChain,err)return}iferr=iptable.AddReturnRule(userChain);err!=nil{logrus.Warnf("无法为%s添加RETURN规则:%v",userChain,err)return}err=iptable。EnsureJumpRule("FORWARD",userChain)iferr!=nil{logrus.Warnf("Failedtoensurethejumprulefor%s:%v",userChain,err)}}可以看到链名固定在code,它将创建/确保链和规则存在。DOCKER-ISOLATION-STAGE-1/2chainDOCKER-ISOLATION-STAGE-1/2这两条链功能相似,这里一并介绍。*filter...:DOCKER-ISOLATION-STAGE-1-[0:0]:DOCKER-ISOLATION-STAGE-2-[0:0]:DOCKER-USER-[0:0]-AFORWARD-jDOCKER-USER-AFORWARD-jDOCKER-ISOLATION-STAGE-1...-ADOCKER-ISOLATION-STAGE-1-idocker0!-odocker0-jDOCKER-ISOLATION-STAGE-2-ADOCKER-ISOLATION-STAGE-1-jRETURN-ADOCKER-ISOLATION-STAGE-2-odocker0-jDROP-ADOCKER-ISOLATION-STAGE-2-jRETURN...这两条链主要是桥接和隔离两个阶段。所谓桥接网络,通常指的是通过Docker创建的接口docker0的网络。/#ifconfigdocker0docker0Linkencap:EthernetHWaddr02:42:11:31:97:0Dinetaddr:172.18.0.1Bcast:172.18.255.255Mask:255.255.0.0UPBROADCASTMTU:1500Metric:1RXpackets:0err:0丢弃:0溢出:0帧:0TX数据包:0错误:0丢弃:0溢出:0载体:0冲突:0txqueuelen:0RX字节:0(0.0B)TX字节:0(0.0B)一个例子来说明。首先创建一个名为moelove的网络并查看其IP。?~dockernetworkcreatemoelove0d3d76dcf81fcf4b9d76ab5a7dec22737b115dddd593c73b27d27f0114cec1e2?~dockerrun--rm-it--networkmoelovealpine/#hostname-i172.22.0.2并启动之前创建的网络容器来ping和使用IP。?~dockerrun--rm-italpineping-c1-w2172.22.0.2PING172.22.0.2(172.22.0.2):56个数据字节---172.22.0.2ping统计数据---1个数据包传输,0个数据包接收,100%丢包?~dockerrun--rm-it--networkmoelovealpineping-c1-w2172.22.0.2PING172.22.0.2(172.22.0.2):56databytes64bytesfrom172.22.0.2:seq=0ttl=64time=0.092ms---172.22.0.2pingstatistics---1packetstransmitted,1packetsreceived,0%packetlossround-tripmin/avg/max=0.092/0.092/0.092ms可以看出,如果是同一网络的容器可以ping通,但是如果是不同网络的容器,则无法ping通。DOCKER-ISOLATION-STAGE-1首先会从桥接网络匹配桥接器,目标是不同的接口。如果匹配,则进入DOCKER-ISOLATION-STAGE-2,如果不匹配,则返回父链。DOCKER-ISOLATION-STAGE-2匹配目标为桥接网络的网桥。如果匹配,则表示该数据包来自桥接网络的网桥,目的地是桥接网络的另一网桥,其DROP被丢弃。.如果不匹配,则返回父链。看到这里,你可能会问为什么要分两个阶段进行隔离?直接用链条隔离可以吗?答案是肯定的,一条链也是可以隔离的,早期版本的Docker就是这么做的。但是那个时候超过30个网络,就会导致Docker启动很慢。所以后面做了这个优化,把这部分的复杂度从O(N^2)降低到O(2N),Docker就不会再启动慢了。DOCKER链最后我们来看一下DOCKER链,它是Docker中使用频率最高的链,也是规则最多的链,但是很容易理解。通常,如果不小心删除了这条链的内容,可能会导致容器的网络出现问题,可以手动解决,也可以通过重启Docker来解决。这里我们启动一个容器,进行端口映射,看看会发生什么变化。(MoeLove)?~dockerexec-it$(dockerps-ql)sh/#dockerrun-p6379:6379--rm-dredis:alpineUnabletofindimage'redis:alpine'locallyalpine:Pullingfromlibrary/redisc158987b0551:Pullcomplete1a990ecc86f0:Pullcompletef2520a938316:Pullcompleteae8c5b65b255:Pullcomplete1f2628236ae0:Pullcomplete329dd56817a5:PullcompleteDigest:sha256:518c024ec78b3074917bad2d40863e882e5297d65587e6d7c6e0b7281d9b8270Status:Downloadednewerimageforredis:alpine6bf21bd3de78ce32617bf64a6a730c0fb50e304509a2ec3ef05ceae648334294/#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES6bf21bd3de78redis:alpine"docker-entrypoint.s..."9secondsagoUp8seconds0.0.0.0:6379->6379/tcpfriendly_spence然后再次执行iptables-save将当前结果与上次比较:*filter+-ADOCKER-d172.18.0.2/32!-idocker0-odocker0-ptcp-mtcp--dport6379-jACCEPT*nat+-APOSTROUTING-s172.18.0.2/32-d172.18.0.2/32-ptcp-mtcp--dport6379-jMASQUERADE+-ADOCKER!-idocker0-ptcp-mtcp--dport6379-jDNAT--to-destination172.18.0.2:6379Docker分别在filter表和nat表中添加了规则。其具体含义如下:filter表中的新规则是指:在自定义DOCKER链中,对于目的地址为172.18.0.2而不是从docker0进入但从docker0退出的TCP协议,目的端口为6379被接受。简单来说,就是释放通过docker0流出的、目的地为172.18.0.2:6379的TCP协议流量。这两条规则在nat表中的表示:对172.18.0.2上目的端口为6379的流量执行MASQUERADE动作(这里可以简单理解为SNAT);在自定义DOCKER链中,如果入口不是docker0且目标端口是6379,执行DNAT动作将目标地址转换为172.18.0.2:6379。简单来说,这条规则为我们提供了对Docker容器进行端口转发的能力,将宿主机本地6379端口流量的目的地址转换为172.18.0.2:6379。当然,要提供完整的访问能力,还需要配合上面列出的其他规则来完成。另外,由于Docker中有很多不同的网络驱动,其他模式也会有一些差异,需要注意。containerd和iptables随着Kubernetes中dockershim的彻底移除,很多人将容器运行时切换到了containerd,甚至有人希望用containerd替换所有的Docker环境。但其实也有一些需要注意的地方。比如我们上面的例子,端口映射(端口发布)在containerd中其实是做不到的。在containerd中,可以使用类似于上述docker的命令启动同一个容器,例如:$ctrrundocker.io/library/redis:alpineredis-1但它没有-p或-P参数。所以发布这个端口的能力是Docker自己提供的。如果你真的想使用这样的功能,你会怎么做呢?一种方式是自己管理iptables规则,但是比较麻烦。另外一种方式,我推荐大家直接使用nerdctl,这是专门为containerd打造的兼容DockerCLI的工具。提供比默认ctr工具丰富得多的许多功能。例如:$nerdctlrun-d--nameredis-1-p6379:6379redis:alpine得到它的IP是192.168.40.9,然后查看iptables的规则:$iptables-tnat-L|grep'192.168.40.9'CNI-66888846605aa0cf860a0834all--192.168.40.9anywhereDNATtcp--anywhereanywheretcpdpt:redisto:192.168.40.9:6379找到了类似的规则,所以可以正常访问。综上所述,本文分析了Docker与iptables的关系,分析了Docker启动后会创建的iptables规则及其含义。还通过实例介绍了Docker端口映射的实际原理,以及如何使用nerdctl配合containerd进行端口映射。容器有很多网络内容,但原理是一样的,类似的内容也包含在Kubernetes中。好了,以上就是本文的内容了。欢迎大家在评论区留言讨论,也请点赞再看,谢谢。欢迎订阅我的文章公众号【MoeLove】