Istio 中的透明流量劫持过程详解
在 Pod 中的 init 容器启动之后,就向 Pod 中增加了 iptables 规则,本文将向你介绍 Istio 中的基于 iptables 的透明流量劫持的全过程。
查看 iptables nat 表中注入的规则
Init 容器通过向 iptables nat 表中注入转发规则来劫持流量的,下图显示的是 productpage 服务中的 iptables 流量劫持的详细过程。
Init 容器启动时命令行参数中指定了 REDIRECT
模式,因此只创建了 NAT 表规则,接下来我们查看下 NAT 表中创建的规则,这是全文中的重点部分,前面讲了那么多都是为它做铺垫的。
productpage
访问 reviews
Pod,入站流量处理过程对应于图示上的步骤:1、2、3、4、Envoy Inbound Handler、5、6、7、8、应用容器。
reviews
Pod 访问 rating
服务的出站流量处理过程对应于图示上的步骤是:9、10、11、12、Envoy Outbound Handler、13、14、15。
上图中关于流量路由部分,包含:
productpage
服务请求访问http://reviews.default.svc.cluster.local:9080/
,当流量进入reviews
Pod 内部时,流量是如何被 iptables 劫持到 Envoy 代理被 Inbound Handler 处理的;reviews
请求访问ratings
服务的 Pod,应用程序发出的出站流量被 iptables 劫持到 Envoy 代理的 Outbound Handler 的处理。
在阅读下文时,请大家确立以下已知点:
- 首先,
productpage
发出的对reivews
的访问流量,是在 Envoy 已经通过 EDS 选择出了要请求的reviews
服务的某个 Pod,知晓了其 IP 地址,直接向该 IP 发送的 TCP 连接请求。 reviews
服务有三个版本,每个版本有一个实例,三个版本中的 sidecar 工作步骤类似,下文只以其中一个 Pod 中的 sidecar 流量转发步骤来说明。- 所有进入
reviews
Pod 的 TCP 流量都根据 Pod 中的 iptables 规则转发到了 Envoy 代理的 15006 端口,然后经过 Envoy 的处理确定转发给 Pod 内的应用容器还是透传。
iptables 规则注入解析
为了查看 iptables 配置,我们需要登陆到 sidecar 容器中使用 root 用户来查看,因为 kubectl
无法使用特权模式来远程操作 docker 容器,所以我们需要登陆到 productpage
pod 所在的主机上使用 docker
命令登陆容器中查看。
如果您使用 minikube 部署的 Kubernetes,可以直接登录到 minikube 的虚拟机中并切换为 root 用户。查看 iptables 配置,列出 NAT(网络地址转换)表的所有规则,因为在 Init 容器启动的时候选择给 istio-iptables
传递的参数中指定将入站流量重定向到 sidecar 的模式为 REDIRECT
,因此在 iptables 中将只有 NAT 表的规格配置,如果选择 TPROXY
还会有 mangle
表配置。iptables
命令的详细用法请参考 iptables 命令。
我们仅查看与 productpage
有关的 iptables 规则如下,因为这些规则是运行在该容器特定的网络空间下,因此需要使用 nsenter
命令进入其网络空间。进入的时候需要指定进程 ID(PID),因此首先我们需要找到 productpage
容器的 PID。对于在不同平台上安装的 Kubernetes,查找容器的方式会略有不同,例如在 GKE 上,执行 docker ps -a
命令是查看不到任何容器进程的。下面已 minikube 和 GKE 两个典型的平台为例,指导你如何进入容器的网络空间。
在 minikube 中查看容器中的 iptabes 规则
对于 minikube,因为所有的进程都运行在单个节点上,因此你只需要登录到 minikube 虚拟机,切换为 root 用户然后查找 productpage
进程即可,参考下面的步骤。
# 进入 minikube 并切换为 root 用户,minikube 默认用户为 docker
$ minikube ssh
$ sudo -i
# 查看 productpage pod 的 istio-proxy 容器中的进程
$ docker top `docker ps|grep "istio-proxy_productpage"|cut -d " " -f1`
UID PID PPID C STIME TTY TIME CMD
1337 10576 10517 0 08:09 ? 00:00:07 /usr/local/bin/pilot-agent proxy sidecar --domain default.svc.cluster.local --configPath /etc/istio/proxy --binaryPath /usr/local/bin/envoy --serviceCluster productpage.default --drainDuration 45s --parentShutdownDuration 1m0s --discoveryAddress istiod.istio-system.svc:15012 --zipkinAddress zipkin.istio-system:9411 --proxyLogLevel=warning --proxyComponentLogLevel=misc:error --connectTimeout 10s --proxyAdminPort 15000 --concurrency 2 --controlPlaneAuthPolicy NONE --dnsRefreshRate 300s --statusPort 15020 --trust-domain=cluster.local --controlPlaneBootstrap=false
1337 10660 10576 0 08:09 ? 00:00:33 /usr/local/bin/envoy -c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0 --drain-time-s 45 --parent-shutdown-time-s 60 --service-cluster productpage.default --service-node sidecar~172.17.0.16~productpage-v1-7f44c4d57c-ksf9b.default~default.svc.cluster.local --max-obj-name-len 189 --local-address-ip-version v4 --log-format [Envoy (Epoch 0)] [%Y-%m-%d %T.%e][%t][%l][%n] %v -l warning --component-log-level misc:error --concurrency 2
# 使用 nsenter 进入 sidecar 容器的命名空间(以上任何一个都可以)
$ nsenter -n --target 10660
# 查看 NAT 表中规则配置的详细信息。
$ iptables -t nat -L
在 GKE 中查看容器的 iptables 规则
如果你在 GKE 中安装的多节点的 Kubernetes 集群,首先你需要确定这个 Pod 运行在哪个节点上,然后登陆到那台主机,使用下面的命令查找进程的 PID,你会得到类似下面的输出。
$ ps aux|grep "productpage"
chronos 4268 0.0 0.6 43796 24856 ? Ss Apr22 0:00 python productpage.py 9080
chronos 4329 0.9 0.6 117524 24616 ? Sl Apr22 13:43 /usr/local/bin/python /opt/microservices/productpage.py 9080
root 361903 0.0 0.0 4536 812 pts/0 S+ 01:54 0:00 grep --colour=auto productpage
然后在终端中输出 iptables -t nat -L
即可查看 iptables 规则。
iptables 流量劫持过程详解
经过上面的步骤,你已经可以查看到 init 容器向 Pod 中注入的 iptables 规则,如下所示。
# PREROUTING 链:用于目标地址转换(DNAT),将所有入站 TCP 流量跳转到 ISTIO_INBOUND 链上。
Chain PREROUTING (policy ACCEPT 2701 packets, 162K bytes)
pkts bytes target prot opt in out source destination
2701 162K ISTIO_INBOUND tcp -- any any anywhere anywhere
# INPUT 链:处理输入数据包,非 TCP 流量将继续 OUTPUT 链。
Chain INPUT (policy ACCEPT 2701 packets, 162K bytes)
pkts bytes target prot opt in out source destination
# OUTPUT 链:将所有出站数据包跳转到 ISTIO_OUTPUT 链上。
Chain OUTPUT (policy ACCEPT 79 packets, 6761 bytes)
pkts bytes target prot opt in out source destination
15 900 ISTIO_OUTPUT tcp -- any any anywhere anywhere
# POSTROUTING 链:所有数据包流出网卡时都要先进入 POSTROUTING 链,内核根据数据包目的地判断是否需要转发出去,我们看到此处未做任何处理。
Chain POSTROUTING (policy ACCEPT 79 packets, 6761 bytes)
pkts bytes target prot opt in out source destination
# ISTIO_INBOUND 链:将所有入站流量重定向到 ISTIO_IN_REDIRECT 链上。目的地为 15090(Prometheus 使用)和 15020(Ingress gateway 使用,用于 Pilot 健康检查)端口的流量除外,发送到以上两个端口的流量将返回 iptables 规则链的调用点,即 PREROUTING 链的后继 POSTROUTING 后直接调用原始目的地。
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN tcp -- any any anywhere anywhere tcp dpt:ssh
2 120 RETURN tcp -- any any anywhere anywhere tcp dpt:15090
2699 162K RETURN tcp -- any any anywhere anywhere tcp dpt:15020
0 0 ISTIO_IN_REDIRECT tcp -- any any anywhere anywhere
# ISTIO_IN_REDIRECT 链:将所有的入站流量跳转到本地的 15006 端口,至此成功的拦截了流量到 sidecar 代理的 Inbound Handler 中。
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15006
# ISTIO_OUTPUT 链:规则比较复杂,将在下文解释
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- any lo 127.0.0.6 anywhere #规则1
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner UID match 1337 #规则2
0 0 RETURN all -- any lo anywhere anywhere ! owner UID match 1337 #规则3
15 900 RETURN all -- any any anywhere anywhere owner UID match 1337 #规则4
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner GID match 1337 #规则5
0 0 RETURN all -- any lo anywhere anywhere ! owner GID match 1337 #规则6
0 0 RETURN all -- any any anywhere anywhere owner GID match 1337 #规则7
0 0 RETURN all -- any any anywhere localhost #规则8
0 0 ISTIO_REDIRECT all -- any any anywhere anywhere #规则9
# ISTIO_REDIRECT 链:将所有流量重定向到 Envoy 代理的 15001 端口。
Chain ISTIO_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15001
这里着重需要解释的是 ISTIO_OUTPUT
链中的 9 条规则,为了便于阅读,我将以上规则中的部分内容使用表格的形式来展示如下:
规则 | target | in | out | source | destination |
---|---|---|---|---|---|
1 | RETURN | any | lo | 127.0.0.6 | anywhere |
2 | ISTIO_IN_REDIRECT | any | lo | anywhere | !localhost owner UID match 1337 |
3 | RETURN | any | lo | anywhere | anywhere !owner UID match 1337 |
4 | RETURN | any | any | anywhere | anywhere owner UID match 1337 |
5 | ISTIO_IN_REDIRECT | any | lo | anywhere | !localhost owner GID match 1337 |
6 | RETURN | any | lo | anywhere | anywhere !owner GID match 1337 |
7 | RETURN | any | any | anywhere | anywhere owner GID match 1337 |
8 | RETURN | any | any | anywhere | localhost |
9 | ISTIO_REDIRECT | any | any | anywhere | anywhere |
下图展示了 ISTIO_ROUTE
规则的详细流程。
- nvoy 代理发出的站内流量(Pod 内部的流量),但不是对 localhost 的请求,通过后续规则将其转发给 Envoy 代理的 Inbound Handler。该规则适用于 Pod 对自身 IP 地址调用的场景。
- 对应图示中的步骤:6 到 7。
- 详情:如果流量的目的地非 localhost,且数据包是由 1337 UID(即
istio-proxy
用户,Envoy 代理)发出的,流量将被经过ISTIO_IN_REDIRECT
最终转发到 Envoy 的 Inbound Handler。
规则 3、6
- 目的:透传 Pod 内的应用容器的站内流量。适用于在应用容器中发出的对本地 Pod 的流量。
- 详情:如果流量不是由 Envoy 用户发出的,那么就跳出该链,返回
OUTPUT
调用POSTROUTING
,直达目的地。
规则 4、7
- 目的:透传 Envoy 代理发出的出站请求。
- 对应图示中的步骤:14 到 15。
- 详情:如果请求是由 Envoy 代理发出的,则返回
OUTPUT
继续调用POSTROUTING
规则,最终直接访问目的地。
规则 8
- 目的:透传 Pod 内部对 localhost 的请求。
- 详情:如果请求的目的地是 localhost,则返回 OUTPUT 调用
POSTROUTING
,直接访问 localhost。
规则 9
- 目的:所有其他的流量将被转发到
ISTIO_REDIRECT
后,最终达到 Envoy 代理的 Outbound Handler。
以上规则避免了 Envoy 代理到应用程序的路由在 iptables 规则中的死循环,保障了流量可以被正确的路由到 Envoy 代理上,也可以发出真正的出站请求。
关于 RETURN target
你可能留意到上述规则中有很多 RETURN target,它的意思是,指定到这条规则时,跳出该规则链,返回 iptables 的调用点(在我们的例子中即 OUTPUT
)后继续执行其余路由规则,在我们的例子中即 POSTROUTING
规则,把流量发送到任意目的地址,你可以把它直观的理解为透传。
关于 127.0.0.6 IP 地址
127.0.0.6 这个 IP 是 Istio 中默认的 InboundPassthroughClusterIpv4
,在 Istio 的代码中指定。即流量在进入 Envoy 代理后被绑定的 IP 地址,作用是让 Outbound 流量重新发送到 Pod 中的应用容器,即 Passthought(透传),绕过 Outbound Handler。该流量是对 Pod 自身的访问,而不是真正的对外流量。至于为什么选择这个 IP 作为流量透传,请参考 Istio Issue-29603。
理解 iptables
为了便于读者理解以上的 iptables 规则,下面将为大家简要介绍下 iptables。
iptables
是 Linux 内核中的防火墙软件 netfilter 的管理工具,位于用户空间,同时也是 netfilter 的一部分。Netfilter 位于内核空间,不仅有网络地址转换的功能,也具备数据包内容修改、以及数据包过滤等防火墙功能。
在了解 Init 容器初始化的 iptables 之前,我们先来了解下 iptables 和规则配置。
下图展示了 iptables 调用链。
iptables 中的表
Init 容器中使用的的 iptables 版本是 v1.6.0
,共包含 5 张表:
raw
用于配置数据包,raw
中的数据包不会被系统跟踪。filter
是用于存放所有与防火墙相关操作的默认表。nat
用于 网络地址转换(例如:端口转发)。mangle
用于对特定数据包的修改(参考损坏数据包)。security
用于强制访问控制 网络规则。
注:在本示例中只用到了 nat
表。
不同的表中的具有的链类型如下表所示:
规则名称 | raw | filter | nat | mangle | security |
---|---|---|---|---|---|
PREROUTING | ✓ | ✓ | ✓ | ||
INPUT | ✓ | ✓ | ✓ | ✓ | |
OUTPUT | ✓ | ✓ | ✓ | ✓ | |
POSTROUTING | ✓ | ✓ | |||
FORWARD | ✓ | ✓ | ✓ | ✓ |
下图是 iptables 的调用链顺序。
iptables 命令
iptables
命令的主要用途是修改这些表中的规则。iptables
命令格式如下:
$ iptables [-t 表名] 命令选项[链名][条件匹配][-j 目标动作或跳转]
Init 容器中的 /istio-iptables.sh
启动入口脚本就是执行 iptables 初始化的。
理解 iptables 规则
查看 istio-proxy
容器中的默认的 iptables 规则,默认查看的是 filter 表中的规则。
$ iptables -L -v
Chain INPUT (policy ACCEPT 350K packets, 63M bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 18M packets, 1916M bytes)
pkts bytes target prot opt in out source destination
我们看到三个默认的链,分别是 INPUT、FORWARD 和 OUTPUT,每个链中的第一行输出表示链名称(在本例中为INPUT/FORWARD/OUTPUT),后跟默认策略(ACCEPT)。
下图是 iptables 的建议结构图,流量在经过 INPUT 链之后就进入了上层协议栈,比如
每条链中都可以添加多条规则,规则是按照顺序从前到后执行的。我们来看下规则的表头定义。
- pkts:处理过的匹配的报文数量
- bytes:累计处理的报文大小(字节数)
- target:如果报文与规则匹配,指定目标就会被执行。
- prot:协议,例如
tdp
、udp
、icmp
和all
。 - opt:很少使用,这一列用于显示 IP 选项。
- in:入站网卡。
- out:出站网卡。
- source:流量的源 IP 地址或子网,后者是
anywhere
。 - destination:流量的目的地 IP 地址或子网,或者是
anywhere
。
还有一列没有表头,显示在最后,表示规则的选项,作为规则的扩展匹配条件,用来补充前面的几列中的配置。prot
、opt
、in
、out
、source
和 destination
和显示在 destination
后面的没有表头的一列扩展条件共同组成匹配规则。当流量匹配这些规则后就会执行 target
。
关于 iptables 规则请参考常见iptables使用规则场景整理。
target 支持的类型
target
类型包括 ACCEPT、REJECT
、DROP
、LOG
、SNAT
、MASQUERADE
、DNAT
、REDIRECT
、RETURN
或者跳转到其他规则等。只要执行到某一条链中只有按照顺序有一条规则匹配后就可以确定报文的去向了,除了 RETURN
类型,类似编程语言中的 return
语句,返回到它的调用点,继续执行下一条规则。target
支持的配置详解请参考 iptables 详解(1):iptables 概念。
从输出结果中可以看到 Init 容器没有在 iptables 的默认链路中创建任何规则,而是创建了新的链路。
参考
- Init 容器 - Kubernetes 中文指南/云原生应用架构实践手册 - jimmysong.io
- JSONPath Support - kubernetes.io
- iptables 命令使用说明 - wangchujiang.com
- How To List and Delete Iptables Firewall Rules - digitalocean.com
- 一句一句解说 iptables的详细中文手册 - cnblog.com
- 理解 Istio Service Mesh 中 Envoy 代理 Sidecar 注入及流量劫持 - jimmysong.io