Istio 中的透明流量劫持过程详解

在 Pod 中的 init 容器启动之后,就向 Pod 中增加了 iptables 规则,本文将向你介绍 Istio 中的基于 iptables 的透明流量劫持的全过程。

查看 iptables nat 表中注入的规则

Init 容器通过向 iptables nat 表中注入转发规则来劫持流量的,下图显示的是 productpage 服务中的 iptables 流量劫持的详细过程。

Envoy sidecar 流量劫持流程示意图
Envoy sidecar 流量劫持流程示意图

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 规则的详细流程。

ISTIO_ROUTE iptables 规则流程图
ISTIO_ROUTE iptables 规则流程图

我将按照规则的出现顺序来解释每条规则的目的、对应文章开头图示中的步骤及详情。其中规则 5、6、7 是分别对规则 2、3、4 的应用范围扩大(从 UID 扩大为 GID),作用是类似的,将合并解释。注意,其中的规则是按顺序执行的,也就是说排序越靠后的规则将作为默认值。出站网卡(out)为 lo (本地回环地址,loopback 接口)时,表示流量的目的地是本地 Pod,对于 Pod 向外部发送的流量就不会经过这个接口。所有 review Pod 的出站流量只适用于规则 4、7、8、9。

规则 1

  • 目的:透传 Envoy 代理发送到本地应用容器的流量,使其绕过 Envoy 代理,直达应用容器。
  • 对应图示中的步骤:6 到 7。
  • 详情:该规则使得所有来自 127.0.0.6(该 IP 地址将在下文解释) 的请求,跳出该链,返回 iptables 的调用点(即 OUTPUT)后继续执行其余路由规则,即 POSTROUTING 规则,把流量发送到任意目的地址,如本地 Pod 内的应用容器。如果没有这条规则,由 Pod 内 Envoy 代理发出的对 Pod 内容器访问的流量将会执行下一条规则,即规则 2,流量将再次进入到了 Inbound Handler 中,从而形成了死循环。将这条规则放在第一位可以避免流量在 Inbound Handler 中死循环的问题。

规则 2、5

  • 目的:处理 Envoy 代理发出的站内流量(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 调用链
iptables 调用链

iptables 中的表

Init 容器中使用的的 iptables 版本是 v1.6.0,共包含 5 张表:

  1. raw 用于配置数据包,raw 中的数据包不会被系统跟踪。
  2. filter 是用于存放所有与防火墙相关操作的默认表。
  3. nat 用于 网络地址转换(例如:端口转发)。
  4. mangle 用于对特定数据包的修改(参考损坏数据包)。
  5. security 用于强制访问控制 网络规则。

:在本示例中只用到了 nat 表。

不同的表中的具有的链类型如下表所示:

规则名称 raw filter nat mangle security
PREROUTING
INPUT
OUTPUT
POSTROUTING
FORWARD

下图是 iptables 的调用链顺序。

iptables 调用链
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 链之后就进入了上层协议栈,比如

iptables结构图
iptables结构图

每条链中都可以添加多条规则,规则是按照顺序从前到后执行的。我们来看下规则的表头定义。

  • pkts:处理过的匹配的报文数量
  • bytes:累计处理的报文大小(字节数)
  • target:如果报文与规则匹配,指定目标就会被执行。
  • prot:协议,例如 tdpudpicmpall
  • opt:很少使用,这一列用于显示 IP 选项。
  • in:入站网卡。
  • out:出站网卡。
  • source:流量的源 IP 地址或子网,后者是 anywhere
  • destination:流量的目的地 IP 地址或子网,或者是 anywhere

还有一列没有表头,显示在最后,表示规则的选项,作为规则的扩展匹配条件,用来补充前面的几列中的配置。protoptinoutsourcedestination 和显示在 destination 后面的没有表头的一列扩展条件共同组成匹配规则。当流量匹配这些规则后就会执行 target

关于 iptables 规则请参考常见iptables使用规则场景整理

target 支持的类型

target 类型包括 ACCEPT、REJECTDROPLOGSNATMASQUERADEDNATREDIRECTRETURN 或者跳转到其他规则等。只要执行到某一条链中只有按照顺序有一条规则匹配后就可以确定报文的去向了,除了 RETURN 类型,类似编程语言中的 return 语句,返回到它的调用点,继续执行下一条规则。target 支持的配置详解请参考 iptables 详解(1):iptables 概念

从输出结果中可以看到 Init 容器没有在 iptables 的默认链路中创建任何规则,而是创建了新的链路。

参考