聊聊 Docker 容器网络和 IPtables 间的亲密关系

公众号关注 「奇妙的 Linux 世界」

设为「星标」,每天带你玩转 Linux !

ca0fd0d504de351ea68e72177e2139bc.png

有小伙伴在群里问到 Docker 和 iptables 的关系,这里来具体聊聊。


Docker 能为我们提供很强大和灵活的网络能力,很大程度上要归功于与 iptables 的结合。在使用时,你可能没有太关注到 iptables 的作用,这是因为 Docker 已经帮我们自动完成了相关的配置。

(MoeLove) ➜  ~ dockerd --help |grep iptables
      --iptables                                Enable addition of iptables rules (default true)

docker daemon 有个 --iptables 的参数,便是用来控制是否要自动启用 iptables 规则的,默认已经设置成了开启(true)。所以通常我们不会过于关注到它的工作。

本文中,为了避免环境的干扰,我将使用 docker in docker 的环境来进行介绍,可通过如下方式启动该环境:

(MoeLove) ➜  ~ docker run --rm -d --privileged docker:dind 
f323aef7b532ba6d575ca6f9444a08f1a55f2447afec2e853954694c034e6ae0

iptables 基础

iptables 是一个用于配置 Linux 内核防火墙的工具,可用于检测、修改转发、重定向以及丢弃 IPv4 数据包。它使用了内核的 ip_tables 的功能,所以需要 Linux 2.4+ 版本的内核。

同时,iptables 为了便于管理,所以按照不同的目的组织了多张 表 ;每张表中又包含了很多预定义的 链;每个链中包含着顺序遍历的 规则;这些规则中又定义了动作的匹配规则和 目标。

对于用户而言,我们通常需要交互的就是 链 和 规则了。

理解 iptables 的主要工作流程有一张比较经典的图:

618da468726c3f7c9fbafed836198967.jpeg

img/tables_traverse.jpg

图片来源:https://www.frozentux.net/iptables-tutorial/images/tables_traverse.jpg

上面的小写字母是 表,大写字母则表示 链,从任何网络端口 进来的每一个 IP 数据包都要从上到下的穿过这张图。

  • 引用自 ArchWiki

不过这不是本篇的重点,所以就不展开了。如果大家对 iptables 的内容感兴趣也欢迎留言,后续可以写一篇完整的。

Docker 网络与 iptables

接下来我们直接看看 Docker 在开启和关闭 iptables 时,具体有什么区别。

关闭 Docker 的 iptables 支持

在本文开头已经为你介绍过 docker daemon 存在一个 --iptables 的参数,用于控制是否使用 iptables 。我们使用以下命令启动一个 docker daemon 并关闭 iptables 支持。

(MoeLove) ➜  ~ docker run --rm -d --privileged docker:dind  dockerd --iptables=false
7135a54c913af5e9ce69a45a0819475503ea9e3c5c673d62d9d38f0f0896179d

进入此容器,并查看其所有 iptables 规则:

(MoeLove) ➜  ~ docker exec -it $(docker ps -ql) sh
/ # iptables-save    
# Generated by iptables-save v1.8.8 on Mon Dec 12 01:46:38 2022
*filter                                                                                              
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [2:80]                                                                                
COMMIT
# Completed on Mon Dec 12 01:46:38 2022

可以看到,当 docker daemon 加了 --iptables=false 的参数时,默认没有任何规则的输出。

开启 Docker 的 iptables 支持

使用以下命令启动一个 docker daemon,这里没有显式的传递 --iptables 选项,因为默认就是 true 。

(MoeLove) ➜  ~ docker run --rm -d --privileged docker:dind             
c464c5c08ecdf9129afbf217c6462236089fe0a1d11dfe7700c2985a04d8d216

查看其 iptables 规则:

(MoeLove) ➜  ~ docker exec -it $(docker ps -ql) sh
/ # iptables-save 
# Generated by iptables-save v1.8.8 on Mon Dec 12 14:48:16 2022
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [1:40]
:POSTROUTING ACCEPT [1:40]
: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.18.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Mon Dec 12 14:48:16 2022
# Generated by iptables-save v1.8.8 on Mon Dec 12 14:48:16 2022
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [2:80]
: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 Mon Dec 12 14:48:16 2022

可以看到,它比刚才关闭 iptables 支持时多了几条链:

  • DOCKER

  • DOCKER-ISOLATION-STAGE-1

  • DOCKER-ISOLATION-STAGE-2

  • DOCKER-USER

以及增加了一些转发规则,以下将具体介绍。

DOCKER-USER 链

在上述新增的几条链中,我们先来看最先生效的 DOCKER-USER 。

*filter
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
...
-A DOCKER-USER -j RETURN

以上规则是在 filter 表中生效的:

  • 第一条是 -A FORWARD -j DOCKER-USER 这表示流量进入 FORWARD 链后,直接进入到 DOCKER-USER 链;

  • 最后一条 -A DOCKER-USER -j RETURN 这表示流量进入 DOCKER-USER 链处理后,(如果无其他处理)可以再 RETURN 回原先的链,进行后续规则的匹配。

这其实是 Docker 预留的一个链,供用户来自行配置的一些额外的规则的。

Docker 默认的路由规则是允许所有客户端访问的, 如果你的 Docker 运行在公网,或者你希望避免 Docker 中容器被局域网内的其他客户端访问,那么你需要在这里添加一条规则。比如, 你仅仅允许 100.84.94.62 访问,但是要拒绝其他客户端访问:

iptables -I DOCKER-USER -i <net interface> ! -s 100.84.94.62 -j DROP

此外,Docker 在重启之类的操作时候,会进行 iptables 相关规则的清理和重建,但是 DOCKER-USER 链中的规则可以持久化,不受影响。

具体的实现均在 docker/libnetwork 下,以下是关于 DOCKER-USER 链的相关代码:

const userChain = "DOCKER-USER"

func arrangeUserFilterRule() {
 if ctrl == nil || !ctrl.iptablesEnabled() {
  return
 }
 iptable := iptables.GetIptable(iptables.IPv4)
 _, err := iptable.NewChain(userChain, iptables.Filter, false)
 if err != nil {
  logrus.Warnf("Failed to create %s chain: %v", userChain, err)
  return
 }

 if err = iptable.AddReturnRule(userChain); err != nil {
  logrus.Warnf("Failed to add the RETURN rule for %s: %v", userChain, err)
  return
 }

 err = iptable.EnsureJumpRule("FORWARD", userChain)
 if err != nil {
  logrus.Warnf("Failed to ensure the jump rule for %s: %v", userChain, err)
 }
}

可以看到链名称是固定在代码中的,同时会创建/确保链和规则存在。

DOCKER-ISOLATION-STAGE-1/2 链

DOCKER-ISOLATION-STAGE-1/2 这两条链作用类似,这里一起进行介绍。

*filter
...
: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 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
...

这两条链主要是分两个阶段进行了桥接网络隔离。所谓的桥接网络,通常就是指通过 docker0 这个由 Docker 创建的接口的网络。

/ # ifconfig docker0
docker0   Link encap:Ethernet  HWaddr 02:42:11:31:97:0D  
          inet addr:172.18.0.1  Bcast:172.18.255.255  Mask:255.255.0.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

举个例子进行说明。

首先创建一个名为 moelove 的 network,并查看它的 IP 。

➜  ~ docker network create moelove
0d3d76dcf81fcf4b9d76ab5a7dec22737b115dddd593c73b27d27f0114cec1e2
➜  ~ docker run --rm -it --network moelove alpine
/ # hostname -i
172.22.0.2

然后分别使用默认的 network 和使用前面创建的 network 启动容器,来 ping 上述创建的容器 IP 。

➜  ~ docker run --rm -it alpine ping -c1 -w2 172.22.0.2  
PING 172.22.0.2 (172.22.0.2): 56 data bytes

--- 172.22.0.2 ping statistics ---
1 packets transmitted, 0 packets received, 100% packet loss


➜  ~ docker run --rm -it --network moelove alpine ping -c1 -w2 172.22.0.2  
PING 172.22.0.2 (172.22.0.2): 56 data bytes
64 bytes from 172.22.0.2: seq=0 ttl=64 time=0.092 ms

--- 172.22.0.2 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.092/0.092/0.092 ms

可以看到,如果是相同 network 的容器是可以 ping 成功的,但如果是不同 network 的容器则不能 ping 通。

DOCKER-ISOLATION-STAGE-1 会首先匹配来自桥接网络的网桥,目标是不同的接口,如果匹配到就进入 DOCKER-ISOLATION-STAGE-2, 不匹配就返回父链。

DOCKER-ISOLATION-STAGE-2 匹配目标是桥接网络的网桥,如果匹配,意味着数据包是来自于一个桥接网络的网桥, 目的地是另一个桥接网络的网桥,并将其 DROP 丢弃掉。不匹配则返回父链。

看到这里,你可能会问 为什么要分两个阶段进行隔离?用一条链直接隔离行不行?

答案是行,一条链也能隔离,Docker 很早的版本就是这样做的。

但是当时的实在超过 30 个 network 以后,就会导致 Docker 启动很慢。所以后来做了这个优化, 将这部分的复杂度从 O(N^2) 降低到 O(2N) ,Docker 就不再会出现启动慢的情况了。

DOCKER 链

最后我们来看看 DOCKER 链,这是 Docker 中使用最为频繁的一个链,也是规则最多的链,但它却很好理解。通常情况下,如果不小心删掉了这个链的内容,可能会导致容器的网络出现问题,手动修复下,或者重启 Docker 均可解决。

这里我们启动一个容器,并进行端口映射,来看看会有哪些变化。

(MoeLove) ➜  ~ docker exec -it $(docker ps -ql) sh
/ # docker run -p 6379:6379 --rm -d redis:alpine
Unable to find image 'redis:alpine' locally
alpine: Pulling from library/redis
c158987b0551: Pull complete 
1a990ecc86f0: Pull complete 
f2520a938316: Pull complete 
ae8c5b65b255: Pull complete 
1f2628236ae0: Pull complete 
329dd56817a5: Pull complete 
Digest: sha256:518c024ec78b3074917bad2d40863e882e5297d65587e6d7c6e0b7281d9b8270
Status: Downloaded newer image for redis:alpine
6bf21bd3de78ce32617bf64a6a730c0fb50e304509a2ec3ef05ceae648334294
/ # docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                    NAMES
6bf21bd3de78   redis:alpine   "docker-entrypoint.s…"   9 seconds ago   Up 8 seconds   0.0.0.0:6379->6379/tcp   friendly_spence

之后再次执行 iptables-save ,对比当前的结果与上次的差别:

*filter
+-A DOCKER -d 172.18.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6379 -j ACCEPT
 *nat
+-A POSTROUTING -s 172.18.0.2/32 -d 172.18.0.2/32 -p tcp -m tcp --dport 6379 -j MASQUERADE
+-A DOCKER ! -i docker0 -p tcp -m tcp --dport 6379 -j DNAT --to-destination 172.18.0.2:6379

Docker 分别在 filter 表和 nat 表增加了规则。它的具体含义如下:

filter 表中新增的这条规则表示:在自定义的 DOCKER 链中,对于目标地址是 172.18.0.2 且不是从 docker0 进入的但从 docker0 出去的,目标端口是 6379 的 TCP 协议则接收。

简单点来说就是放行通过 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 中还存在多种不同的 network 驱动,在其他模式下还会有一些区别,需要注意。

containerd 与 iptables

随着 Kubernetes 中将 dockershim 彻底移除,已经有很多人将容器运行时切换到了 containerd,甚至有人希望把所有 Docker 环境都替换成 containerd。但这里其实有一些需要注意的点,比如我们上述的示例,在 containerd 中实际上是无法进行端口映射(端口发布)的。

containerd 中可以通过类似上述 docker 的命令来启动相同的容器,比如:

$ ctr run docker.io/library/redis:alpine redis-1

但它是没有 -p 或者 -P 参数的。所以这个端口发布的能力是 Docker 自己专门提供的。

如果确实想用这样的功能,怎么做呢?

一种方式是自己来管理 iptables 规则,但比较繁琐了。

另一种方式,推荐大家可以直接使用 nerdctl 这是一个专为 containerd 做的, 兼容 Docker CLI 的工具。提供了很多远比默认的 ctr 工具更丰富的能力。

比如可以这样:

$ nerdctl run -d --name redis-1 -p 6379:6379 redis:alpine

获取其 IP 是 192.168.40.9, 然后检查 iptables 的规则:

$ iptables -t nat -L | grep '192.168.40.9'
CNI-66888846605aa0cf860a0834  all  --  192.168.40.9    anywhere             
DNAT       tcp  --  anywhere           anywhere        tcp dpt:redis to:192.168.40.9:6379

发现有类似的规则,让它可以正常访问。

总结

本篇从 Docker 与 iptables 的关系将其,分别剖析了 Docker 启动后会创建的 iptables 规则及其含义。并通过示例介绍了 Docker 端口映射的实际原理, 以及如何利用 nerdctl 配合使用 containerd 进行端口映射。

容器的网络内容比较多,不过原理都是相通的,在 Kubernetes 中也包含了类似的内容。

好了,以上就是本篇的内容。

本文转载自:「MoeLove」,原文:https://url.hi-linux.com/NA0F8,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com。

8383fbc9890ee407c3428d206a3ca33f.gif

最近,我们建立了一个技术交流微信群。目前群里已加入了不少行业内的大神,有兴趣的同学可以加入和我们一起交流技术,在 「奇妙的 Linux 世界」 公众号直接回复 「加群」 邀请你入群。

29cd0e9cc94a7a4ce61f2dafa9b53582.png

你可能还喜欢

点击下方图片即可阅读

b3cbc8e35584db88ec0c358b84bbdb69.jpeg

Showcode: 一款用于快速建立超高颜值代码图像的工具

31b171f120aa77c233a4b4d4a9217ba0.png
点击上方图片,『美团|饿了么』外卖红包天天免费领

dc31f5380f2eb87b4a758d1604d66ccd.png

更多有趣的互联网新鲜事,关注「奇妙的互联网」视频号全了解!

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值