随着软件架构不断的演进,更加快捷部署大型复杂应用成为了人们关注的方向。近几年Service Mesh开始不断出现在大家的视野里,越来越多的大厂开始对其进行实践。事实上在Service Mesh发展中核心就是不断对Sidecar模式配备更加完善的流量管理解决方案。
在Service Mesh中Sidecar是透明的,开发者对其无感知的,而其中对于流量最关键的一步就是Sidecar进行的流量劫持,那么Sidecar又是如何进行流量劫持呢?最常见的方式就是iptables,本文将会带你了解iptables,同时明白Istio中是如何使用iptables进行流量劫持。

Service Mesh介绍

Service Mesh中文译名服务网格,是用以处理服务与服务之间通信的基础设施层。功能在于处理服务间通信,职责是实现请求的可靠传递。在实践中,Service Mesh通常由服务与轻量级网络代理(Sidecar,与服务部署在一起)组合而成。它在原有的客户端和服务端之间加了一个代理,但对应用程序透明。

当其扩大到一定规模后,就会形成网格状(Mesh),即形成了Service Mesh的形态:

Service Mesh中这些相互连接的网络代理设计允许我们更优雅地管理东西流量和南北流量。它将底层那些难以控制的网络通讯统一管理,而上层的应用层协议只需关心业务逻辑即可。Service Mesh是一个用于处理服务间通信的基础设施层,它负责为构建复杂的云原生应用传递可靠的网络请求。

Sidecar模式

最早在2003年,Netflix发现了在微服务上调用跨语言的问题,一些情况完全单一的技术栈比较困难,语言都会有一些特定非常适用的场景,而编写大量的SDK比较费时间,还要为每个语言都编写一套。所以最后就想到了使用Sidecar模式,把SDK中的功能移植到Sidecar里面。
实际上Sidecar就是一个部署在本地的代理服务器。它能接管入口和出口流量,类比Web Server中Nginx+php-fpm这种模式,只是协议由HTTP变为了FastCGI,不过Nginx只是代理了入口流量,并没有出口流量。

在云原生的环境下,使用Sidecar模式部署Service Mesh服务网格时,无需在节点上运行代理,但是集群中将运行多个相同的sidecar副本。在Sidecar部署方式中,每个应用的容器旁都会部署一个伴生容器(如 Envoy 或 MOSN),这个容器称之为Sidecar容器。Sidecar接管进出应用容器的所有流量。在Kubernetes的Pod中,在原有的应用容器旁边注入一个Sidecar容器,两个容器共享存储、网络等资源,可以广义的将这个包含了Sidecar容器的Pod理解为一台主机,两个容器共享主机资源。

iptables基础

iptables是一个运行在用户空间的应用软件,它通过控制Linux内核netfilter模块,来管理网络数据包的流动与转送。实际上netfilter才是防火墙真正的安全框架(framework)它位于内核空间,而iptables其实是一个命令行工具,位于用户空间,我们用这个工具可以操作真正的框架。
netfilter/iptables(下文中简称为iptables)组成Linux平台下的包过滤防火墙,与大多数的Linux软件一样,这个包过滤防火墙是免费的,它可以代替昂贵的商业防火墙解决方案,完成封包过滤、封包重定向和网络地址转换(NAT)等功能。

规则

iptables的规则就是根据指定的匹配条件来尝试匹配每个流经此处的报文,一旦匹配成功,则由规则后面指定的处理动作进行处理。
简单来说iptables是按照规则来办事的,就好比如果数据包头符合什么样的条件,就这样处理这个数据包。这些规则存储在内核空间的信息包过滤表中,其中包括对原地址、目的地址、传输协议等的限制,当其匹配时就会通过定义的方法来处理这些数据包,如ACCEPT、REJECT、DROP等。配置iptables的主要工作就是添加、修改和删除这些规则。
综上,规则由匹配条件和处理动作组成。

匹配条件

匹配条件分为基本匹配条件与扩展匹配条件,如源地址、目标地址、源端口、目标端口等。

处理动作

  • ACCEPT,允许封包通过,如果在子链ACCEPT,则相当于在父链也ACCEPT了,不会继续遍历父链后续规则
  • RETURN,从当前链返回,行为类似于编程语言函数中的return。
  • DROP,直接丢弃数据包,不进行后续处理
  • REJECT,返回connection refused或者destination unreachable报文
  • QUEUE,将数据包放入用户空间的队列,供用户空间应用程序处理
  • JUMP,跳转到用户自定义链继续执行
  • LOG 让内核记录匹配的封包
  • AUDIT 对封包进行审计
  • DNAT 目标地址映射
  • SNAT 源地址映射
  • MASQUERADE 源地址映射,用于本机地址是动态获取的情况下
  • REDIRECT 重定向,主要用于实现端口重定向

通常情况下,一旦匹配了某个规则,就会执行它的目标。同时,不再检查同规则链、同表的后续规则,这意味着规则的顺序非常重要。某些目标是例外,例如LOG、MARK,会继续遍历后面的规则。
如果内置规则链执行到结尾,或者内置规则链中的某个目标是RETURN的规则匹配封包,那么规则链策略(Chain Policy)中定义的目标将决定如何处理当前封包。

五表五链

表是用于分类管理iptables规则的。不同的表,通常用作不同的目的:

  1. filter表:用于控制到达某条链条上的数据包如何处理,是放行(Accept)、丢弃(Drop)还是拒绝(Reject)。如果不指定-t参数,这是默认操控的表格
  2. nat表:用于修改源地址,或者目的地址。该表格在某个创建了新连接的封包出现时生效
  3. mangle表:该表格用于修改封包的内容
  4. raw表:具有高优先级,可以对接收到的封包在连接跟踪(Connection Tracking,为了支持NAT,iptables会对每个L4连接进行跟踪,也就是维护每个L4连接的状态)前进行处理(也就是取消跟踪),可以应用在那些不需要做NAT的情况下提高性能。例如高访问的Web服务,可以不让iptables做80端口封包的连接跟踪
  5. security表:用于强制访问控制(Mandatory Access Control),很少使用

其优先级从高到低为 raw ⇨ mangle ⇨ nat ⇨ filter ⇨ security

netfilter是Linux网络安全大厦的基石,从2.4开始引入。它提供了一整套Hook函数机制,IP层的5个钩子点对应了iptables的5个内置链条:

  1. PREROUTING,在此DNAT
  2. POSTROUTING,在此SNAT
  3. INPUT,处理输入给本地进程的封包
  4. OUTPUT,处理本地进程输出的封包
  5. FORWARD,处理转发给其他机器、其他网络命名空间的封包

对于从网络接口入站的IP封包,首先进入PREROUTING链,然后进行路由判断:

  • 如果封包路由目的地是本机,则进入INPUT链,然后发给本地进程
  • 如果封包路由目的地不是本机,并且启用了IP转发,则进入FORWARD链,然后通过POSTROUTING链,最后经过网络接口发走
  • 对于本地进程发往协议栈的封包,则首先通过OUTPUT链,然后通过POSTROUTING链,最后经过网络接口发走

除此以外,实际上我们还可以自定义链,而自定义链只能被某个默认的链当做动作去调用才能起作用。

表和链的关系

需要注意,不是任何链条上可以挂任何表:

  1. raw可以挂在PREROUTING、OUTPUT
  2. mangle可以挂在任何链上
  3. nat(SNAT)可以挂在POSTROUTING、INPUT
  4. nat(DNAT)可以挂在PREROUTING、OUTPUT
  5. filter可以挂在FORWARD、INPUT、OUTPUT
  6. security可以挂在FORWARD、INPUT、OUTPUT

 

数据包处理流程

  • 经过本机的包:prerouting ->input
  • 从本机转发出去的包:prerouting->forward->postrouting
  • 从本机出去的包:output->postrouting

Istio中的iptables流量劫持

Istio是目前服务网络的代表之一,但是istio又不仅仅是服务网格。在 Linkerd, Envoy 这样的典型服务网格之上,istio提供了一个完整的解决方案,为整个服务网格提供行为洞察和操作控制,可以满足微服务应用程序的多样化需求。
这里我们会以Istio为例,解析其是如何通过Sidecar使用iptables进行流量劫持。
首先需要明白的是在Kubernetes中Istio通过Admission webhook的机制将Envoy Sidecar自动注入于应用容器运行于同一个Pod中,这种情况下它们将共享网络名称空间,因此也使用同一个网络协议栈。
Istio 给应用 Pod 注入的配置主要包括:

  • Init 容器 istio-init:用于Pod中设置iptables端口转发
  • Sidecar 容器 istio-proxy:运行Envoy Sidecar代理

根据我们关注的内容,我们主要来看其Init容器。

我们可以看到该容器的启动命令如下,

其入口为/usr/local/bin/istio-iptables,对应代码https://github.com/istio/istio/tree/master/tools/istio-iptables
该命令行工具的用法如下:

而以上传入的参数都会重新组装成iptables规则。
可见我们最开始的启动命令作用为:

  • 将容器的出站流量都重定向到Envoy Sider的15001端口
  • 将容器的入站流量都重定向到Envoy Sider的15006端口
  • 使用istio-proxy用户身份运行,UID 1337
  • 使用REDIRECT模式来重定向流量
  • 排除重定向到Sidcar的入站端口为15090、15021、15020,他们是Istio的管理端口

当proxy作为Sidecar运行时,启用的是Envoy的REDIRECT模式,此时进程为–uid-owner 1337;当proxy作为透明代理运行时,启用的是Envoy的TPROXY模式,此时进程为–gid-owner 1337。
注意-u-m的作用,就是其为指定用户空间id,满足其余规则的情况下由该用户空间发出的数据包不被iptables转发,这可以防止由Envoy发出的数据包又被iptables转发到Envoy,形成死循环。

在容器初始化后我们通过进入Sidecar容器后切换为root用户来查看配置的iptables规则。

对其总结来说:

  • ISTIO_INBOUND 链:所有进入Pod但非指定端口(如22)的流量全部重定向至15006端口(Envoy入口流量端口)进行拦截处理。
  • ISTIO_OUTPUT 链:所有流出Pod由 istio-proxy 用户空间发出且目的地不为localhost的流量全部重定向至15001端口(envoy出口流量端口),其他流量全部直接放行至下一个POSTROUTING链,不用被Envoy拦截处理。

其中对应出现的除SSH外的端口功能如下:

综上我们不难理解,Istio是如何通过iptables来完成流量劫持,除了一部分特定功能的端口外,在用户请求到达Pod时对应流量会被拦截到Sidecar处理,并由Sidecar请求业务服务,响应时同理。
规则更详细的配置信息可以通过如下的命令来查看。

下图展示了ProductPage服务的请求访问过程。以Review服务为例,接收请求后向Ratings服务发送请求。

在这张图中我们可以观察当流量进入Reviews服务内部时,Reviews服务内部的Sidecar Proxy是如何做流量拦截和路由转发的。不过图中序号15,16标注应该是有些问题,在这里我们对应前面的规则,重新绘制了一个更通俗易懂的图并进行分析。

  1. 进入Pod的Inbound流量首先被PREROUTING链拦截并处理
  2. 当TCP请求进入PREROUTING链时全部交给ISTIO_INBOUND处理 -A PREROUTING -p tcp -j ISTIO_INBOUND
  3. 请求目标端口非15008/22/15090/15021/15020的TCP请求全部交给ISTIO_IN_REDIRECT处理 -A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
  4. 将发送到此的TCP请求全部重定向至15006端口(Envoy入口流量端口)-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
  5. 在Envoy内部处理后,决定将数据包转发到应用,这一步对Envoy来说属于出口流量,会被netfilter拦截转发至出口流量OUTPUT链
  6. 出站请求,当TCP请求进入OUTPUT链时全部交给ISTIO_OUTPUT处理 -A OUTPUT -p tcp -j ISTIO_OUTPUT
  7. 匹配出站请求对应规则,请求本地服务,出口为lo网卡同时来自istio-proxy用户空间,流量返回到它的调用点中的下一条链执行,即POSTROUTING链 -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN / -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
  8. Sidecar发出的请求到达目标应用
  9. 目标应用处理完业务逻辑后响应Sidecar,这一步对应用来说属于出口流量,再次被netfilter拦截转发至出口流量OUTPUT链
  10. 出站请求,当TCP请求进入OUTPUT链时全部交给ISTIO_OUTPUT处理 -A OUTPUT -p tcp -j ISTIO_OUTPUT
  11. 请求下一个服务/响应请求,即请求非本地服务同时不来自istio-proxy用户空间,流量被转发至ISTIO_REDIRECT链 -A ISTIO_OUTPUT -j ISTIO_REDIRECT
  12. 将重定向于此的TCP协议请求流量全部重定向至15001端口(Envoy出口流量端口)-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
  13. 在Envoy内部处理后,决定将数据包对外转发,这一步对Envoy来说属于出口流量,会被netfilter拦截转发至出口流量OUTPUT链
  14. 出站请求,当TCP请求进入OUTPUT链时全部交给ISTIO_OUTPUT处理 -A OUTPUT -p tcp -j ISTIO_OUTPUT
  15. 请求非本地的服务,出口不为lo网卡同时来自istio-proxy用户空间则跳过了ISTIO_REDIREC处理,直接RETURN到下一个链,即POSTROUTING链 -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN / -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
  16. POSTROUTING链处理完成后,根据路由表选择合适的网卡发送Outbound流量

能看出来入站的数据包的处理比较简单,除了一些端口排除外,其余的都进入proxy入站处理逻辑,除了上面的常规情况以下是ISTIO_OUTPUT链中处理特殊情况的规则:

  • 127.0.0.6是InboundPassthroughBindIpv4,代表是PassThroughCluster来源的流量,直接跳过不劫持。这是Envoy中最后生效的Cluster,如果流量没有匹配到规则,即我们访问了一个没有暴露到svc,的端口,则会透传到服务容器。
  • 对于由Envoy经由lo网卡发出的目的地非127.0.0.1的数据包,进入Envoy的入站处理端口;这条规则主要应对的容器调用本身所在的svc,并且经过Envoy代理之后调度到自己的情况(即appN <=> Envoy (client) <=> Envoy (server) <=> appN),这种情况下目的IP是当前Pod自己的IP,但是数据是由Envoy发出的(uid/gid owner 1337),这种情况也属于数据包入站的case,所以直接进入Envoy的入站端口,进行后续转发处理逻辑。
  • 如果是来自lo网卡的业务容器通过自己的Pod IP调用自己的数据包(appN => appN by lo)即app容器通过自己的Pod IP调用自己所在的Pod内的服务的情况,不走Envoy处理流程,直接返回。
  • app容器通过localhost调用自己所在的Pod内的服务的情况,不做任何处理,直接返回。

流量劫持的研究方向

用iptables进行流量劫持是最经典、最通用的手段。不过,因为iptables重定向流量必须通过回环设备(Loopback)交换数据,所以流量不得不多穿越一次协议栈。

这种方案在网络I/O不构成主要瓶颈的系统中并没有什么不妥,但在网络敏感的大并发场景下会因转发而损失一定的性能。目前,如何实现更优化的数据平面流量劫持,是服务网格发展的前沿研究课题之一,其中一种可行的优化方案是使用eBPF(Extended Berkeley Packet Filter)技术,在Socket层面直接完成数据转发,而不需要再往下经过更底层的TCP/IP协议栈的处理,从而减少它数据在通信链路的路径长度。

另一种可以考虑的方案是让服务网格与CNI插件配合来实现流量劫持,譬如Istio就有提供自己实现的CNI插件。只要安装了这个CNI插件,整个虚拟化网络都由Istio自己来控制,那自然就无需再依赖iptables,也不必存在initContainers配置和istio-init容器了。这种方案有很高的上限与自由度,不过,要实现一个功能全面、管理灵活、性能优秀、表现稳定的CNI网络插件决非易事,连Kubernetes自己都迫不及待想从网络插件中脱坑,其麻烦程度可见一斑,因此目前这种方案使用得也并不算广泛。
流量劫持技术的发展与服务网格的落地效果密切相关,有一些服务网格通过基座模式中的SDK也能达到很好的转发性能,但考虑到应用程序通用性和环境迁移等问题,无侵入式的低时延、低管理成本的流量劫持方案仍然是研究的主流方向。

参考

https://jimmysong.io/blog/sidecar-injection-iptables-and-traffic-routing
https://www.sohu.com/a/370467539_262549
https://www.zsythink.net/archives/1199
https://blog.gmem.cc/iptables
https://zhuanlan.zhihu.com/p/98193165
https://www.linux-note.cn/?p=2485
https://bbs.huaweicloud.com/blogs/172037
https://mosn.io/docs/concept/traffic-hijack/
http://icyfenix.cn/immutable-infrastructure/mesh/communication.html
https://blog.csdn.net/cloudvtech/article/details/106532419
https://www.cnblogs.com/whych/p/9147900.html
https://blog.csdn.net/weixin_43490818/article/details/89314360
https://github.com/istio/istio/blob/master/tools/istio-iptables/pkg/cmd/run.go