【Nginx】使用NGINX作为HTTPS正向代理服务器


NGINX 主要设计作为反向代理服务器,但随着 NGINX 的发展,它同样能作为正向代理的选项之一。正向代理本身并不复杂,而如何代理加密的 HTTPS 流量是正向代理需要解决的主要问题。本文将介绍利用 NGINX 来正向代理 HTTPS 流量两种方案,及其使用场景和主要问题。

HTTP / HTTPS 正向代理的分类

简单介绍下正向代理的分类作为理解下文的背景知识:

按客户端有无感知的分类

  • 普通代理:在客户端需要在浏览器中或者系统环境变量手动设置代理的地址和端口如鱿鱼,在客户端指定鱿鱼服务器 IP 和端口 3128。
  • 透明代理:客户端不需要做任何代理设置,“代理”这个角色对于客户端是透明的。如企业网络链路中的 Web Gateway 设备。

按代理是否解密 HTTPS 的分类

  • 隧道代理:。也就是透传代理代理服务器只是在 TCP 协议上透传 HTTPS 流量,对于其代理的流量的具体内容不解密不感知客户端和其访问的目的服务器做直接 TLS / SSL 交互本文中讨论的 NGINX 代理方式属于这种模式。
  • 中间人(MITM,Man-in-the-Middle)代理:代理服务器解密 HTTPS 流量,对客户端利用自签名证书完成 TLS / SSL 握手,对目的服务器端完成正常 TLS 交互。在客户端 - 代理 - 服务器的链路中建立两段 TLS / SSL 会话。如Charles,简单原理描述可以参考文章
  • 注:这种情况客户端在 TLS 握手阶段实际上是拿到的代理服务器自己的自签名证书,证书链的验证默认不成功,需要在客户端信任代理自签证书的根 CA 证书。所以过程中是客户端有感的。如果要做成无感的透明代理,需要向客户端推送自建的根 CA 证书,在企业内部环境下是可实现的。

为什么正向代理处理 HTTPS 流量需要特殊处理?

作为反向代理时,代理服务器通常终结(终止)HTTPS 加密流量,再转发给后端实例.HTTPS 流量的加解密和认证过程发生在客户端和反向代理服务器之间。

而作为正向代理在处理客户端发过来的流量时,HTTP 加密封装在了 TLS / SSL 中,代理服务器无法看到客户端请求的 URL 中想要访问的域名,如下图。所以代理 HTTPS 流量,相比于 HTTP,需要做一些特殊处理。

NGINX 的解决方案

根据前文中的分类方式,NGINX 解决 HTTPS 代理的方式都属于透传(隧道)模式,即不解密不感知上层流量。具体的方式有如下 7 层和 4 层的两类解决方案。

HTTP CONNECT 隧道(7 层解决方案)

历史背景

早在 1998 年,也就是 TLS 还没有正式诞生的 SSL 时代,主导 SSL 协议的 Netscape 公司就提出了关于利用 web 代理来隧道 SSL 流量的INTERNET-DRAFT。其核心思想就是利用 HTTP CONNECT 请求在客户端和代理之间建立一个 HTTP CONNECT Tunnel,在 CONNECT 请求中需要指定客户端需要访问的目的主机和端口.Draft 中的原图如下:

整个过程可以参考 HTTP 权威指南中的图:

  1. 客户端给代理服务器发送 HTTP CONNECT 请求。
  2. 代理服务器利用 HTTP CONNECT 请求中的主机和端口与目的服务器建立 TCP 连接。
  3. 代理服务器给客户端返回 HTTP 200 响应。
  4. 4.客户端和代理服务器建立起 HTTP CONNECT 隧道,HTTPS 流量到达代理服务器后,直接通过 TCP 透传远端目的服务器。代理服务器的角色是透传 HTTPS 流量,并不需要解密 HTTPS。

NGINX ngx_http_proxy_connect_module 模块

NGINX 作为反向代理服务器,官方一直没有支持 HTTP CONNECT 方法。但是基于 NGINX 的模块化,可扩展性好的特性,阿里的 @chobits 提供了ngx_http_proxy_connect_module模块,来支持 HTTP CONNECT 方法,从而让 NGINX 可以扩展为正向代理。

环境搭建

以 CentOS 7 的环境为例。

1)安装

对于新安装的环境,参考正常的安装步骤和安装这个模块的步骤(https://github.com/chobits/ngx_http_proxy_connect_module)),把对应版本的补丁打开之后,在 configure 的时候加上参数 - -add 模块= /路径/到/ ngx_http_proxy_connect_module,示例如下:

./configure \--user=www \--group=www \--prefix=/usr/local/nginx \--with-http_ssl_module \--with-http_stub_status_module \--with-http_realip_module \--add-module=/root/src/ngx_http_proxy_connect_module

对于已经安装编译安装完的环境,需要加入以上模块,步骤如下:

2)nginx.conf 文件配置

使用场景

7 层需要通过 HTTP CONNECT 来建立隧道,属于客户端有感知的普通代理方式,需要在客户端手动配置 HTTP(S)代理服务器 IP 和端口。在客户端用 curl 加-x 参数访问如下:

从上面-v 参数打印出的细节,可以看到客户端先往代理服务器 39.105.196.164 建立了 HTTP CONNECT 隧道,代理回复 HTTP / 1.1 200 连接建立后就开始交互 TLS / SSL 握手和流量了。

NGINX 流(4 层解决方案)

既然是使用透传上层流量的方法,那可不可做成“4 层代理”,对 TCP / UDP 以上的协议实现彻底的透传呢?答案是可以的.NGINX 官方从 1.9.0 版本开始支持ngx_stream_core_module模块,模块默认不建立,需要配置时加上–with 流选项来开启。

问题

用 NGINX stream 在 TCP 层面上代理 HTTPS 流量肯定会遇到本文一开始提到的那个问题:代理服务器无法获取客户端想要访问的目的域名。因为在 TCP 的层面获取的信息仅限于 IP 和端口层面,没有任何机会拿到域名信息。要拿到目的域名,必须要有拆上层报文获取域名信息的能力,所以 NGINX stream 的方式不是完全严格意义上的 4 层代理,还是要略微借助些上层能力。

ngx_stream_ssl_preread_module 模块

要在不解密的情况下拿到 HTTPS 流量访问的域名,只有利用 TLS / SSL 握手的第一个客户端 Hello 报文中的扩展地址 SNI(服务器名称指示)来获取.NGINX 官方从 1.11.5 版本开始支持利用ngx_stream_ssl_preread_module模块来获得这个能力,模块主要用于获取客户端 Hello 报文中的 SNI 和 ALPN 信息。对于 4 层正向代理来说,从客户端 Hello 报文中提取 SNI 的能力是至关重要的,否则 NGINX stream 的解决方案无法成立。同时这也带来了一个限制,要求所有客户端都需要在 TLS / SSL 握手中带上 SNI 字段,否则 NGINX stream 代理完全没办法知道客户端需要访问的目的域名。

环境搭建

1)安装

对于新安装的环境,参考正常的安装步骤,直接在 configure 的时候加上–with-stream, - with-stream_ssl_preread_module 和–with-stream_ssl_module 选项即可。示例如下:

对于已经安装编译安装完的环境,需要加入以上 3 个与流相关的模块,步骤如下:

2)nginx.conf 文件配置

NGINX stream 与 HTTP 不同,需要在流块中进行配置,但是指令参数与 HTTP 块都是类似的,主要配置部分如下:

使用场景

对于 4 层正向代理,NGINX 对上层流量基本上是透传,也不需要 HTTP CONNECT 来建立隧道。适合于透明代理的模式,比如将访问的域名利用 DNS 解定向到代理服务器。我们可以通过在客户端绑定/ etc / hosts 中来模拟。

在客户端:

常见问题

1)客户端手动设置代理导致访问不成功

4 层正向代理是透传上层 HTTPS 流量,不需要 HTTP 连接来建立隧道,也就是说不需要客户端设置 HTTP(S)代理。如果我们在客户端手动设置 HTTP(S)代理是否能访问成功呢?我们可以用 curl -x 来设置代理为这个正向服务器访问测试,看看结果:

可以看到客户端试图于正向 NGINX 前建立 HTTP CONNECT 隧道,但是由于 NGINX 是透传,所以把 CONNECT 请求直接转发给了目的服务器。目的服务器不接受 CONNECT 方法,所以最终出现“Proxy CONNECT aborted”,导致访问不成功。

2)客户端没有带 SNI 导致访问不成功

上文提到用 NGINX 流做正向代理的关键因素之一是利用 ngx_stream_ssl_preread_module 提取出客户端中的 SNI 字段。如果客户端客户端不携带 SNI 字段,会造成代理服务器无法获知目的域名的情况,导致访问不成功。

在透明代理模式下(用手动绑定承载的方式模拟),我们可以在客户端用的 OpenSSL 来模拟:

openssl s_client 默认不带 SNI,可以看到上面的请求在 TLS / SSL 握手阶段,发出客户端 Hello 后就结束了。因为代理服务器不知道要把客户端 Hello 往哪个目的域名转发。

如果用 OpenSSL 的带服务器名参数来指定 SNI,则可以正常访问成功,命令如下:

总结

本文总结了 NGINX 利用 HTTP CONNECT 隧道和 NGINX 流两种方式做 HTTPS 正向代理的原理,环境搭建,使用场景和主要问题,希望给大家在做各种场景的正向代理时提供参考。