聊聊容器网络和 iptables

标签: post Linux Docker Kubernetes | 发表时间:2022-12-12 00:04 | 作者:[email protected] (张晋涛)
出处:https://moelove.info/

大家好,我是张晋涛。

上周有小伙伴在群里问到 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 的主要工作流程有一张比较经典的图:

img/tables_traverse.jpg

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

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

不过这不是本篇的重点,所以就不展开了。如果大家对 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】

TheMoeLove

相关 [容器 网络 iptables] 推荐:

聊聊容器网络和 iptables

- - MoeLove
上周有小伙伴在群里问到 Docker 和 Iptables 的关系,这里来具体聊聊. Docker 能为我们提供很强大和灵活的网络能力,很大程度上要归功于与 iptables 的结合. 在使用时,你可能没有太关注到 iptables 的作用,这是因为 Docker 已经帮我们自动完成了相关的配置. docker daemon 有个 --iptables 的参数,便是用来控制是否要自动启用 iptables 规则的,默认已经设置成了开启(true).

Kubernetes Service iptables 网络通信验证

- - 三点水
Kubernetes gives Pods their own IP addresses and a single DNS name for a setof Pods, and can load-balance across them.. K8s Service会为每个 Pod 都设置一个它自己的 IP,并为一组 Pod 提供一个统一的 DNS 域名,还可以提供在它们间做负载均衡的能力.

iptables NAT 学习

- - BlogJava-首页技术区
为了搞清楚iptables NAT的过程,做了这个实验. 使用了1台双网卡服务器和1台单网卡服务器,2个网段. 1.       为了看到调度服务器上的数据转发过程,首先在调度服务器上分出内核的debug日志:. l 在/etc/rsyslog.conf最后增加:kern.debug /var/log/iptables.log.

iptables 小结

- - CSDN博客系统运维推荐文章
        最近工作上一个作业用到了iptables命令,主要进行端口映射,在网上查了好多资料,尽管有很多例子,但还是整了好几天才整明白. (有一些是从网络中总结的,不断完善中...). (1) iptables简介.            iptables是一个Linux下优秀的nat+防火墙工具,iptables操作的是2.4以上内核的netfilter,所以需要linux的内核在2.4以上.

iptables 详解

- - 行业应用 - ITeye博客
以下文章转载于:http://blog.chinaunix.net/uid-26495963-id-3279216.html. 防火墙,其实说白了讲,就是用于实现Linux下访问控制的功能的,它分为硬件的或者软件的防火墙两种. 无论是在哪个网络中,防火墙工作的地方一定是在网络的边缘. 而我们的任务就是需要去定义到底防火墙如何工作,这就是防火墙的策略,规则,以达到让它对出入网络的IP、数据进行检测.

iptables原理说明

- - CSDN博客互联网推荐文章
1.iptables的前身叫ipfirewall (内核1.x时代),这是一个作者从freeBSD上移植过来的,能够工作在内核当中的,对数据包进行检测的一款简易访问控制工具. 2. 作者一共在内核空间中选择了5个位置,.     1).内核空间中:从一个网络接口进来,到另一个网络接口去的.     2).数据包从内核流入用户空间的.

iptables 学习总结

- - 小火箭
参考文章: 朱双印 iptables. 主机防火墙:对单个主机进行防护. 网络防火墙:通常处于网络的入口/出口,服务于其背后的局域网. 硬件防火墙:在硬件级别实现部分防火墙功能,另一部分功能基于软件实现,性能高,成本高. 软件防火墙:应用软件处理逻辑运行于通用硬件平台之上的防火墙,性能低,成本低.

linux下IPTABLES配置详解

- - Linux - 操作系统 - ITeye博客
作者博客:http://www.cnblogs.com/JemBai/. 原文地址:http://www.cnblogs.com/JemBai/archive/2009/03/19/1416364.html. 如果你的IPTABLES基础知识还不了解,建议先去看看. 我们来配置一个filter表的防火墙.

容器网络并不难

- - DockOne.io
【编者的话】本文通过实验方法一步步揭秘容器网络是如何实现容器间的互通,以及容器和外部网络是如何连通的. 使用容器总是感觉像使用魔法一样. 对于那些理解底层原理的人来说容器很好用,但是对于不理解的人来说就是个噩梦. 很幸运的是,我们已经研究容器技术很久了,甚至成功揭秘 容器只是隔离并受限的Linux进程, 运行容器并不需要镜像,以及另一个方面, 构建镜像需要运行一些容器.

利用iptables 封与解封IP

- - 傻子-王跸西的blog-WangBiXi.com
I表示Insert(添加),-D表示Delete(删除).