首页 > 上网技巧 > iptables 及 docker 容器网络分析

iptables 及 docker 容器网络分析

时间:2021-09-07 08:50 作者:QQ地带 我要评论

本文仅针对 ipv4 网络
 
本文先介绍 iptables 的基本概念及常用命令,然后分析 docker/podman 是如何利用 iptables 和 Linux 虚拟网络接口实现的单机容器网络。
 
一、iptables
iptables 提供了包过滤、NAT 以及其他的包处理能力,iptables 应用最多的两个场景是 firewall 和 NAT
 
iptables 及新的 nftables 都是基于 netfilter 开发的,是 netfilter 的子项目。
 
但是 eBPF 社区目前正在开发旨在取代 netfilter 的新项目 bpfilter,他们的目标之一是兼容 iptables/nftables 规则,让我们拭目以待吧。
 
iptables 基础概念 - 四表五链
实际上还有张 SELinux 相关的 security 表(应该是较新的内核新增的,但是不清楚是哪个版本加的),但是我基本没接触过,就略过了。
 
详细的说明参见 iptables详解(1):iptables概念 - 朱双印,这篇文章写得非常棒!把 iptables 讲清楚了。
 
默认情况下,iptables 提供了四张表(不考虑 security 的话)和五条链,数据在这四表五链中的处理流程如下图所示:
 
在这里的介绍中,可以先忽略掉图中 link layer 层的链路,它属于 ebtables 的范畴。另外 conntrack 也暂时忽略,在下一小节会详细介绍 conntrack 的功能。
 
/images/netfilter/netfilter-packet-flow.pngnetfilter 数据包处理流程,来自 wikipedia
对照上图,对于发送到某个用户层程序的数据而言,流量顺序如下:
 
首先进入 PREROUTING 链,依次经过这三个表: raw -> mangle -> nat
然后进入 INPUT 链,这个链上也有三个表,处理顺序是:mangle -> nat -> filter
过了 INPUT 链后,数据才会进入内核协议栈,最终到达用户层程序。
用户层程序发出的报文,则依次经过这几个表:OUTPUT -> POSTROUTING
 
从图中也很容易看出,如果数据 dst ip 不是本机任一接口的 ip,那它通过的几个链依次是:PREROUTEING -> FORWARD -> POSTROUTING
 
五链的功能和名称完全一致,应该很容易理解。下面按优先级分别介绍下链中的四个表:
 
raw: 对收到的数据包在连接跟踪前进行处理。一般用不到,可以忽略
一旦用户使用了 RAW 表,RAW 表处理完后,将跳过 NAT 表和 ip_conntrack 处理,即不再做地址转换和数据包的链接跟踪处理了
mangle: 用于修改报文、给报文打标签
nat: 主要用于做网络地址转换,SNAT 或者 DNAT
filter: 主要用于过滤数据包
数据在按优先级经过四个表的处理时,一旦在某个表中匹配到一条规则 A,下一条处理规则就由规则 A 的 target 参数指定,后续的所有表都会被忽略。target 有如下几种类型:
 
ACCEPT: 直接允许数据包通过
DROP: 直接丢弃数据包,对程序而言就是 100% 丢包
REJECT: 丢弃数据包,但是会给程序返回 RESET。这个对程序更友好,但是存在安全隐患,通常不使用。
MASQUERADE: (伪装)将 src ip 改写为网卡 ip,和 SNAT 的区别是它会自动读取网卡 ip。路由设备必备。
SNAT/DNAT: 顾名思义,做网络地址转换
REDIRECT: 在本机做端口映射
LOG: 在/var/log/messages文件中记录日志信息,然后将数据包传递给下一条规则,也就是说除了记录以外不对数据包做任何其他操作,仍然让下一条规则去匹配。
只有这个 target 特殊一些,匹配它的数据仍然可以匹配后续规则,不会直接跳过。
其他类型,可以用到的时候再查
理解了上面这张图,以及四个表的用途,就很容易理解 iptables 的命令了。
 
常用命令
注意: 下面提供的 iptables 命令做的修改是未持久化的,重启就会丢失!在下一节会简单介绍持久化配置的方法。
 
命令格式:
 
1
iptables [-t table] {-A|-C|-D} chain [-m matchname [per-match-options]] -j targetname [per-target-options]
其中 table 默认为 filter 表,但是感觉系统管理员实际使用最多的是 INPUT 表,用于设置防火墙。
 
以下简单介绍在 INPUT 表上添加、修改规则,来设置防火墙:
 
本文后续分析时,假设用户已经清楚 linux bridge、veth 等虚拟网络接口相关知识。 如果你还缺少这些前置知识,请先阅读文章 Linux 中的虚拟网络接口。
 
conntrack 连接跟踪与 NAT
在讲 conntrack 之间,我们再回顾下前面给出过的 netfilter 数据处理流程图:
 
/images/netfilter/netfilter-packet-flow.pngnetfilter 数据包处理流程,来自 wikipedia
上一节中我们忽略了图中的 conntrack,它就是本节的主角——netfilter 的连接跟踪(connection tracking)模块。
 
netfilter/conntrack 是 iptables 实现 SNAT/DNAT/MASQUERADE 的前提条件,上面的流程图显示, conntrack 在 PREROUTEING 和 OUTPUT 表的 raw 链之后生效。
 
下面以 docker 默认的 bridge 网络为例详细介绍下 conntrack 的功能。
 
首先,这是我在「Linux 的虚拟网络接口」文中给出过的 docker0 网络架构图:
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+-----------------------------------------------+-----------------------------------+-----------------------------------+
|                      Host                     |           Container A             |           Container B             |
|                                               |                                   |                                   |
|   +---------------------------------------+   |    +-------------------------+    |    +-------------------------+    |
|   |       Network Protocol Stack          |   |    |  Network Protocol Stack |    |    |  Network Protocol Stack |    |
|   +----+-------------+--------------------+   |    +-----------+-------------+    |    +------------+------------+    |
|        ^             ^                        |                ^                  |                 ^                 |
|........|.............|........................|................|..................|.................|.................|
|        v             v  ↓                     |                v                  |                 v                 |
|   +----+----+  +-----+------+                 |          +-----+-------+          |           +-----+-------+         |
|   | .31.101 |  | 172.17.0.1 |      +------+   |          | 172.17.0.2  |          |           |  172.17.0.3 |         |
|   +---------+  +-------------<---->+ veth |   |          +-------------+          |           +-------------+         |
|   |  eth0   |  |   docker0  |      +--+---+   |          | eth0(veth)  |          |           | eth0(veth)  |         |
|   +----+----+  +-----+------+         ^       |          +-----+-------+          |           +-----+-------+         |
|        ^             ^                |       |                ^                  |                 ^                 |
|        |             |                +------------------------+                  |                 |                 |
|        |             v                        |                                   |                 |                 |
|        |          +--+---+                    |                                   |                 |                 |
|        |          | veth |                    |                                   |                 |                 |
|        |          +--+---+                    |                                   |                 |                 |
|        |             ^                        |                                   |                 |                 |
|        |             +------------------------------------------------------------------------------+                 |
|        |                                      |                                   |                                   |
|        |                                      |                                   |                                   |
+-----------------------------------------------+-----------------------------------+-----------------------------------+
         v
    Physical Network  (192.168.31.0/24)
docker 会在 iptables 中为 docker0 网桥添加如下规则:
 
1
2
3
4
-t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
 
-t filter -P DROP
-t filter -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
这几行规则使 docker 容器能正常访问外部网络。MASQUERADE 在请求出网时,会自动做 SNAT,将 src ip 替换成出口网卡的 ip. 这样数据包能正常出网,而且对端返回的数据包现在也能正常回到出口网卡。
 
现在问题就来了:出口网卡收到返回的数据包后,还能否将数据包转发到数据的初始来源端——某个 docker 容器?难道 docker 还额外添加了与 MASQUERADE 对应的 dst ip 反向转换规则?
 
实际上这一步依赖的是本节的主角——iptables 提供的 conntrack 连接跟踪功能(在「参考」中有一篇文章详细介绍了此功能)。
 
连接跟踪对 NAT 的贡献是:在做 NAT 转换时,无需手动添加额外的规则来执行反向转换以实现数据的双向传输。netfilter/conntrack 系统会记录 NAT 的连接状态,NAT 地址的反向转换是根据这个状态自动完成的。
 
比如上图中的 Container A 通过 bridge 网络向 baidu.com 发起了 N 个连接,这时数据的处理流程如下:
 
首先 Container A 发出的数据包被 MASQUERADE 规则处理,将 src ip 替换成 eth0 的 ip,然后发送到物理网络 192..168.31.0/24。
conntrack 系统记录此连接被 NAT 处理前后的状态信息,并将其状态设置为 NEW,表示这是新发起的一个连接
对端 baidu.com 返回数据包后,会首先到达 eth0 网卡
conntrack 查表,发现返回数据包的连接已经记录在表中并且状态为 NEW,于是它将连接的状态修改为 ESTABLISHED,并且将 dst_ip 改为 172.17.0.2 然后发送出去
注意,这个和 tcp 的 ESTABLISHED 没任何关系
经过路由匹配,数据包会进入到 docker0,然后匹配上 iptables 规则:-t filter -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT,数据直接被放行
数据经过 veth 后,最终进入到 Container A 中,交由容器的内核协议栈处理。
数据被 Container A 的内核协议栈发送到「发起连接的应用程序」。
实际测试 conntrack
现在我们来实际测试一下,看看是不是这么回事:
 
1
2
3
# 使用 tcpdump 分别在出口网卡 wlp4s0 (相当于 eth0)和 dcoker0 网桥上抓包,后面会用来分析
❯ sudo tcpdump -i wlp4s0 -n > wlp4s0.dump   # 窗口一,抓 wlp4s0 的包
❯ sudo tcpdump -i docker0 -n > docker0.dump  # 窗口二,抓 docker0 的包
现在新建窗口三,启动一个容器,通过 curl 命令低速下载一个视频文件:
 
1
❯ docker run --rm --name curl -it curlimages/curl "https://media.w3.org/2010/05/sintel/trailer.mp4" -o /tmp/video.mp4 --limit-rate 100k
然后新建窗口四,在宿主机查看 conntrack 状态
 
1
2
3
4
5
6
❯ sudo zypper in conntrack-tools  # 这个记得先提前安装好
❯ sudo conntrack -L | grep 172.17
# curl 通过 NAT 网络发起了一个 dns 查询请求,DNS 服务器是网关上的 192.168.31.1
udp      17 22 src=172.17.0.4 dst=192.168.31.1 sport=59423 dport=53 src=192.168.31.1 dst=192.168.31.228 sport=53 dport=59423 [ASSURED] mark=0 use=1
# curl 通过 NAT 网络向 media.w3.org 发起了 tcp 连接
tcp      6 298 ESTABLISHED src=172.17.0.4 dst=198.18.5.130 sport=54636 dport=443 src=198.18.5.130 dst=192.168.31.228 sport=443 dport=54636 [ASSURED] mark=0 use=1
等 curl 命令跑个十来秒,然后关闭所有窗口及应用程序,接下来进行数据分析:
 
能看到数据确实在进入 docker0 网桥前,dst_ip 确实被从 192.168.31.228(wlp4s0 的 ip)被修改为了 172.17.0.4(Container A 的 ip).
 
NAT 如何分配端口?
上一节我们实际测试发现,docker 容器的流量在经过 iptables 的 MASQUERADE 规则处理后,只有 src ip 被修改了,而 port 仍然是一致的。
 
但是如果 NAT 不修改连接的端口,实际上是会有问题的。如果有两个容器同时向 ip: 198.18.5.130, port: 443 发起请求,又恰好使用了同一个 src port,在宿主机上就会出现端口冲突! 因为这两个请求被 SNAT 时,如果只修改 src ip,那它们映射到的将是主机上的同一个连接!
 
这个问题 NAT 是如何解决的呢?我想如果遇到这种情况,NAT 应该会通过一定的规则选用一个不同的端口。
 
有空可以翻一波源码看看这个,待续…
 
如何持久化 iptables 配置
首先需要注意的是,centos7/opensuse 15 都已经切换到了 firewalld 作为防火墙配置软件, 而 ubuntu18.04 lts 也换成了 ufw 来配置防火墙。
 
包括 docker 应该也是在启动的时候动态添加 iptables 配置。
 
对于上述新系统,还是建议直接使用 firewalld/ufw 配置防火墙吧,或者网上搜下关闭 ufw/firewalld、启用 iptables 持久化的解决方案。
 
本文主要目的在于理解 docker 容器网络的原理,以及为后面理解 kubernetes 网络插件 calico/flannel 打好基础,因此就不多介绍持久化了。
 
如何使用 iptables + bridge + veth 实现容器网络
Docker/Podman 默认使用的都是 bridge 网络,它们的底层实现完全类似。下面以 docker 为例进行分析(Podman 的分析流程也基本一样)。
 
通过 docker run 运行容器
首先,使用 docker run 运行几个容器,检查下网络状况:
 
接下来使用如下 docker-compose 配置启动一个 caddy 容器,添加自定义 network 和端口映射,待会就能验证 docker 是如何实现这两种网络的了。
 
docker-compose.yml 内容:
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
version: "3.3"
services:
  caddy:
    image: "caddy:2.2.1-alpine"
    container_name: "caddy"
    restart: always
    command: caddy file-server --browse --root /data/static
    ports:
      - "8081:80"
    volumes:
      - "/home/ryan/Downloads:/data/static"
    networks:
    - caddy-1
 
networks:
  caddy-1:
现在先用上面的配置启动 caddy 容器,然后再查看网络状况:
 
到这里,我们简单地分析了下 docker 如何通过 iptables 实现 bridge 网络和端口映射。 有了这个基础,后面就可以尝试深入分析 kubernetes 网络插件 flannel/calico/cilium 了哈哈。
 
Docker/Podman 的 macvlan/ipvlan 模式
注意:macvlan 和 wifi 好像不兼容,测试时不要使用无线网络的接口!
 
我在前面介绍 Linux 虚拟网络接口的文章中,有介绍过 macvlan 和 ipvlan 两种新的虚拟接口。
 
目前 Podman/Docker 都支持使用 macvlan 来构建容器网络,这种模式下创建的容器直连外部网络,容器可以拥有独立的外部 IP,不需要端口映射,也不需要借助 iptables.
 
这和虚拟机的 Bridge 模式就很类似,主要适用于希望容器拥有独立外部 IP 的情况。
 
下面详细分析下 Docker 的 macvlan 网络(Podman 应该也完全类似)。
 
Docker 支持的另一种网络模式是 ipvlan(ipvlan 和 macvlan 的区别我在前一篇文章中已经介绍过,不再赘言),创建命令和 macvlan 几乎一样:
 
Rootless 容器的网络实现
如果容器运行时也在 Rootless 模式下运行,那它就没有权限在宿主机添加 bridge/veth 等虚拟网络接口,这种情况下,我们前面描述的容器网络就无法设置了。
 
那么 podman/containerd(nerdctl) 目前是如何在 Rootless 模式下构建容器网络的呢?
 
查看文档,发现它们都用到了 rootlesskit 相关的东西,而 rootlesskit 提供了 rootless 网络的几个实现,文档参见 rootlesskit/docs/network.md
 
其中目前推荐使用,而且 podman/containerd(nerdctl) 都默认使用的方案,是 rootless-containers/slirp4netns
 
以 containerd(nerdctl) 为例,按官方文档安装好后,随便启动几个容器,然后在宿主机查 iptables/ip addr ls,会发现啥也没有。 这显然是因为 rootless 模式下 containerd 改不了宿主机的 iptables 配置和虚拟网络接口。但是可以查看到宿主机 slirp4netns 在后台运行:
 
1
2
❯ ps aux | grep tap
ryan     11644  0.0  0.0   5288  3312 ?        S    00:01   0:02 slirp4netns --mtu 65520 -r 3 --disable-host-loopback --enable-sandbox --enable-seccomp 11625 tap0
但是我看半天文档,只看到怎么使用 rootlesskit/slirp4netns 创建新的名字空间,没看到有介绍如何进入一个已存在的 slirp4netns 名字空间…
 
使用 nsenter -a -t 11644 也一直报错,任何程序都是 no such binary…
 
以后有空再重新研究一波…
 
总之能确定的是,它通过在虚拟的名字空间中创建了一个 tap 虚拟接口来实现容器网络,性能相比前面介绍的网络多少是要差一点的。
 
nftables
前面介绍了 iptables 以及其在 docker 和防火墙上的应用。但是实际上目前各大 Linux 发行版都已经不建议使用 iptables 了,甚至把 iptables 重命名为了 iptables-leagacy.
 
目前 opensuse/debian/opensuse 都已经预装了并且推荐使用 nftables,而且 firewalld 已经默认使用 nftables 作为它的后端了。
 
我在 opensuse tumbleweed 上实测,firewalld 添加的是 nftables 配置,而 docker 仍然在用旧的 iptables,也就是说我现在的机器上有两套 netfilter 工具并存:
 
但是现在 kubernetes/docker 都还是用的 iptables,nftables 我学了用处不大,以后有空再补充。

标签: iptables
顶一下
(0)
0%
踩一下
(0)
0%

Google提供的广告