云开发 CloudBase(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等 Serverless 化能力,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用、Flutter 客户端等)。
本文详细介绍了云开发的网关架构设计迁移历程,为什么从双层架构演变成单层架构,对业界有较强的参考作用。
01、引言
「云开发网关」是以 Envoy 为底座的一款面向 APP、微信/企业微信小程序、公众号 H5/web 的云开发安全接入网关,提供云开发私有链路、流量治理、弱网加速等能力,为应用提供安全、稳定、云原生的接入。
安全链路是「云开发网关」(以下简称网关)的核心能力,网关使用了私密链路为用户流量安全、反爬等提供了底层支持。
1.1 HTTPS 存在的问题
全球使用 HTTPS 网站已经超过了 9 成,HTTPS 本身使用 TLS 对业务请求的流量已经进行加密,以现有的 TLS1.2/1.3 的加密强度和当前的算力来讲,暴力破解可以说几无可能。那么网关使用私密链路再次对业务流量加密,是否有必要呢?其实是有必要的,针对 HTTPS 攻击者可以使用 MITM 来获取客户端和服务器传入流量。
通过 MITM 来解密 HTTPS 的流量,需要客户端去信任中间人颁发的第三方根证书;而安装根证书本身有一些门槛。所以,在常见的攻击方式中,攻击者通常并不会使用这种方式。一是这种攻击条件通常需要攻击者和被攻击者在同一个局域网下,二是信任攻击者的根证书需要被攻击者配合,同时也需要管理员或者 root 权限。使用 MITM 苛刻条件使这种方式在实际的攻击中并不常见,更多的则是使用 Msfvenom 生成载荷,来诱骗被攻击者执行,从而获取机器的权限。既然 MITM 很难直接被利用,是不是在我们业务场景就可以忽略这种安全风险呢?在一般的业务中,确实可以认为使用 HTTPS 就已经达到了安全的需求,而在一些对安全场景要求较高的领域,只使用 HTTPS 还是不够的。比如,电商平台的价格、挂号平台的号源信息;竞对可以通过 MITM 方式实时监听友商的商品价格,做到自己平台价格的实时调整,从而保证自己的低价优势;黄牛可以实时查询号源,在放号后第一时间进行挂号等等。
1.2 针对 MITM 的措施
既然 MITM 本身对业务来说存在一定的风险,通常情况下该怎么避免呢?
- 使用 mTLS 进行双向认证。
- 使用 SSL Pinning 做域名和证书的绑定校验。
- 检查用户网络是否使用代理或者 VPN(银行 APP 常用)。
- 对业务再做一层加密,使用私密链路进行传输。
mTLS 的本质是客户端和服务端证书的双向认证,在 APP 场景下通常可以这么解决,然而真实的业务一般都要求全端支持,H5/Web、微信小程序的场景下,并没有权限获取到证书的信息。SSL Pinning 同样有类似的问题。无论 mTLS 或者 SSL Pinning 在校验证书的时候通常都依赖操作系统提供的系统 API 进行校验,而系统 API 很容易使用 Xposed、Frida 等工具进行绕过。通过检查用户代理的方式同样也不那么可靠,其判断的依据仍然依赖系统 API,同样,攻击者也可以使用 Tun 虚拟网卡的方式绕过。
使用私密链路对业务数据进行一次加密,可以很好的解决上面的问题,而且具有更好的兼容性和扩展性;即使是多端的场景,使用同一套解决方案也可以很好的处理。私密链路除了带来链路的安全性,也可以隐藏服务端的真实业务,一些自动化爬虫和攻击脚本由于不清楚私密链路的具体协议,对应的请求会被网关直接拒绝掉。
02、双层架构设计
经过网关的流量都是 HTTP (L7 层)流量,一个标准的 HTTP 请求包含:请求行(Request line)、请求头部(Request Header)、请求消息体(Request Body)三个部分;HTTP 返回包含:响应状态(Status line)、响应首部(Response Header)、响应消息体(Response Body)三个部分。
在实际业务中,一些客户会使用 URL 参数来实现自己的签名等鉴权敏感信息,还有一些客户会将敏感信息放到请求的头部中去。如果只对请求的消息体进行加密,用户的鉴权信息仍然可能被拦截和篡改。这就要求网关不但要保护请求的消息体,也要对请求的头部和请求行进行保护;同样的对于业务的返回响应状态、响应首部、响应消息体也要进行保护。
如果直接对请求的各个部分进行加密处理,加密后直接转发到网关,网关以同样的方式进行解密,似乎就可以解决面临的问题。但是,直接加密转发的请求并不是一个标准的 HTTP,那么请求的流量从 L7 层也就降级到了 L4 层处理。针对 APP 这种场景使用 L4 也十分合理,不过 H5/Web 的场景却受到了限制,现代浏览器仍然不支持 Raw Socket 的连接,这就会导致 APP 的设计和 H5/Web 的架构很难统一。
2.1 业务流量封装
无论 APP,H5/Web 还是微信小程序,都支持 HTTP 的请求。这就要求我们的架构设计,底层也需要基于 HTTP 来实现。出于安全性的考虑,又需要对业务的请求行、请求头部、请求消息体进行加密,那么使用 HTTP in HTTP 的传输方式就更加合适。将业务的请求行信息、头部和消息体经过加密后放到私密链路的消息体后再进行转发,再结合一些序列化方式(比如:Protocol Buffers)来压缩请求数据,即可以保证较高的性能,又可以有较小传输长度。
针对不同类型客户端,可以采用分发 SDK 的方式集成到业务。业务的客户端使用 SDK 去调用 HTTP 请求,由 SDK 来完成请求的加密。除此之外,业务的 SDK 还可以添加埋点信息,在出现业务故障时,结合日志、告警机制可以更及时的发现问题。
2.2 早期架构设计
转发到网关的流量需要解密后才能做进一步的处理,因此在早期的设计方案中。最先考虑的也是添加一层加解密模块的方式来处理。对应设计为:
客户端 HTTP -> 网关 SDK -> 加解密模块 -> 网关集群(底层 Envoy,通常对应 DownStream)-> 回源业务服务(通常称为 Upstream)
加解密模块需要大量的 CPU 运算来处理业务的请求,因此在部署的时候更适合集群部署,只要配置合理的 HPA 就基本可以满足业务的需要。除了 CPU 外,还需要考虑一些特殊场景,比如:秒杀的场景下,业务服务可能因为负载增加,导致请求的耗时增加;耗时的增加也意味短时间内连接数的积累,而短时间的请求数可能会进一步增加,最终可能会导致链路的某一环超过负载而彻底拒绝服务。Upstream 的耗时增加可能会带来灾难性的后果,针对这种场景,加解密模块到网关集群的请求需要做池化处理,要尽可能的复用连接;同样也要配置合适的超时时间和连接保持时间,对于已经失败的请求要快速失败来优化这种场景。对于超出连接池最大数量的请求,是直接拒绝还是移除掉较早的连接,同样需要结合业务实际的场景来考虑。
两层的架构很好的适应了早期的业务场景,不过也存在一些缺陷:
- 加解密模块缺少必要健康检查和全死全活逻辑
- 加解密模块监控信息不够完善,业务指标需要主动注册 Promtheus;添加新的指标需要重新发布服务
- 增加了资源成本和维护成本
在双层架构的场景下,加解密模块充当整个链路的第一跳,在高并发场景下是首当其冲的。不过加解密的性能、连接数并不是其主要的瓶颈;一方面加解密模块采用 Go 协程来处理每个请求,其性能可以有很好的保证;另外,Go C10k 早就不是问题,反而是加解密模块作为客户端请求时,其 IP 固定,基于连接的四元组可知,请求的本地端口可能因为异常情况而占满,导致无法创建新的请求,不过有上述的池化保证,也不需要太过担心。
03、单层架构设计
网关的底层采用了开源的 Envoy 来进行流量的转发,而 Envoy 本身就有丰富的监控信息以及完善的健康检查逻辑。那么是否可以将加解密模块合并到 Envoy 呢?完全可以,不过一些技术难点需要解决。在双层架构中,Envoy 处理的流量就是业务的流量,因此可以根据某些头部做集中式限频,动态的增加和删除某些头部,或者根据某些信息添加风险等级。
在单层架构中,Envoy 实际处理的流量是 HTTP in HTTP 的外层流量,即私密链路的流量,因此需要解决以下问题:
- Envoy 怎么集成加解密模块?
- Envoy 对每个请求,怎么解析出业务流量后;覆盖私密链路的请求,即将私密链路流量替换成业务流量?
- 如何保证请求后的解密流程在限频等逻辑之前执行,返回后的加密流程在限频等逻辑之后执行?
除此之外,由于采用 HTTP in HTTP,还要考虑原业务的 cookie、跨域信息在内层流量:
- 怎样保证私密链路情况下,业务 Set-Cookie 可以正常执行。
- 如何正确处理业务跨域头部(比如:Access-Control-Allow-Origin、Access-Control-Allow-Headers 等)等等。
单层架构也是一个云开发各类网关统一架构演进的方向,因此除了要考虑私密链路的场景,针对一些公网直接访问以及 WebSocket 的场景也要进行兼容。
3.1 Envoy 的拦截器
Envoy 提供了多种拦截器(Envoy Filter),可以动态的过滤、修改、监听某些字段,通过 Envoy Filter 可以实现更为复杂的业务逻辑。比较常用的拦截器有 Lua Filter、External Processing Filter 等。
Lua Filter 本身实现较为轻量,经常用于处理一些简单的业务场景。不过,由于 Envoy 本身是多 Worker 线程处理机制,每个 Worker 都有自己的 Lua 执行环境,这也就导致 Lua Filter 没有真正意义上的全局变量。另外,Lua Filter 在处理每个请求的时候都是同步执行的,如果需要执行一些网络 IO 操作,就会导致 Envoy 的性能大幅下降。所以,网关只会在少量修改请求或者对性能要求极高的时候才会结合使用 Lua Fitler。
External Processing Filter(以下简称 gRPC 拦截器) 提供了 gRPC 接口供远程调用,可以动态的修改请求和返回的几乎所有数据,这正是网关私密链路这种场景所需要的。gRPC 拦截器会将一个请求拆成 4 个 gRPC 串行调用
- ProcessingRequest_RequestHeaders,请求头部处理。
- ProcessingRequest_RequestBody,请求 Body 处理。
- ProcessingRequest_ResponseHeaders,返回头部处理。
- ProcessingRequest_ResponseBody,返回 Body 处理。
一个加密的请求到网关之后,首先会接收到 RequestHeaders 消息,对于 RequestHeaders 处理会比较简单,判断是否为探测 OPTIONS 或内部健康检查,如果是则直接返回 204;再根据请求的 Origin 动态的返回跨域所需要的头部即可。RequestBody 携带了业务的完整请求信息,需要先解密再做 HTTP Parser 获取业务请求行、请求头部和消息体;然后将解析后的信息,覆盖掉请求的头部和消息体;改为单层网关后,Envoy 就充当了整个链路的第一跳,还需要把请求的 X-Forwarded-For 复写为 Remote 地址来防止伪造的可能。返回的头部和请求的头部处理基本一样,一个不同点就是 Set Cookie 支持多个字段,这里需要合并处理。对于 ResponseBody 需要重新打包加密后再进行返回;由于业务的状态码可能存在异常,而私密链路本身是应该正常返回,所以这里并不能业务的状态码复写到私密链路,以免请求异常中断。
3.2 拦截器的顺序
使用 gRPC 拦截器解决了流量加解密的问题,不过多个 Filter 的协作仍然需要处理。Envoy 在请求的时候,执行的拦截器的顺序是自上而下;返回的处理恰好相反,自下而上。
因此,在请求的时候先经过 Lua Request 的预处理,私密链路的 gRPC 拦截器再进行解密,解密后的流量重新发到限频/防水墙的时候,已经是业务的数据了。在返回的时候同样经过 Lua Reponse 的预处理,再使用私密链路的 gRPC 拦截器进行封装,这样整个链路就打通了。
04、总结
通常增加一层中间层可以解决业务所遇到的问题,不过增加一层映射也带来新的问题;增加中间层同样需要计算资源的支撑,为了高可用势必需要增加多副本,这就降低了整个系统的 ROI。在网关这个场景下,通过合并加解密层到网关接入层,由双层架构演变成单层架构,网关的架构进一步统一,日志、监控、告警也可以直接复用;在当前大背景下反而是一个更优解。
-End-
原创作者|李皓宇