温故而知新之 https proxy

周六昏昏欲睡的下午,顺手用 goproxy 写了个小工具。功能部分不到两个小时就搞定了,但是在处理 https proxy 部分,希望实现一个自定义feature时,调试了挺长时间。这大大打击了自诩可以手撕 https/tls 的自我信心😝。于是顺手看了一下这块的实现部分。

原理上,https proxy 的处理,都是以客户端 CONNECT 请求开始,后续的请求都是通过这次建立的连接进行 req-rsp 交互。一句话就能讲完,很简单,对吧?对也不对。正确的部分在于,原理的确就是这样的,但是如何使用这个连接,以及如何处理其中的安全问题让这一块有很多细节需要考虑。所谓魔鬼在细节中,这也是这块最有意思的地方。

以 goproxy 这块的实现为例,通过 CONNECT 建立连接以后,请求的的交互主要支持4种方式

  • ConnectAccept
  • ConnectHijack
  • ConnectHTTPMitm
  • ConnectMitm

ConnectAccept 是最基本的方式,只负责在 tcp 层建立远端和客户端的连接,这个连接具体怎么用,由客户端自己与远端交互决定。各个平台、各个客户端都支持这种方式,基本上没有兼容性问题。

ConnectHijack 与后面的两种方式本质上同一种类型,都可以认为是 Hijack 类型。这里涉及一个很重要的概念 Hijack, 这个在 golang 标准库中有非常准确详细的解释,搬运如下:

也就是相较于 ConnectAccept, proxy 不再是中间的小透明,而是可以接管连接,在中间实现一些自定义有意思的功能,这也是 MITM 的基石。所不同的是:

  • ConnectHijack 要求在业务自己在应用层实现这块逻辑,优点就是一切尽在掌握,可以实现很多有意思的功能。
  • ConnectHTTPMitm 是一种 https 降级为 http 的一种实现。也就是 https 通过这个 proxy 后,都被降级为 http 请求与远端交互。优点是可以 offload TLS 这层的加解密和签名开销,但是完全没有安全性。因此,这种一种古老的方式已经不被绝大部分 client 支持了。
  • ConnectMitm 则是一种经典实现,把客户端的 https 请求在中间做中转,然后还是以 https 的方式发送到远端。这是被client广泛支持的方式,因此兼容性较好。

需要说明的是:

  • Hijack 类型都依赖CA证书,这也是为什么你手机、电脑设备里面的CA根证书很重要,不要随便信任和安装来路不明证书的原因。
  • goproxy 在 ConnectHTTPMitmConnectMitm 的实现中,都会再次调用 filterRequest 执行 OnRequest 的 handler, 因此要在你的 request handler 中识别和处理这种经过转换的请求,否则会出现 loop 以及请求失败的情况。

从原理到实现,基本就拆解完了。其实也没有什么高深的部分,正如很多东西,不过是温故而知新,进一寸有一寸的欢喜。