NGINX最初被设计为反向代理服务器。但是,随着不断发展,NGINX也可以作为实现转发代理的选项之一。转发代理本身并不复杂,它解决的关键问题是如何加密HTTPS流量。本文介绍了使用NGINX作为HTTPS流量转发代理的两种方法,以及它们的应用场景和主要问题。
今天87cloud详谈下阿里云国际如何使用NGINX作为HTTPS转发代理服务器
HTTP/HTTPS 转发代理的分类
首先,让我们仔细看看转发代理的分类。
分类依据:代理对客户是否透明
- 通用代理:在这里,代理地址和端口是在客户端上的浏览器或系统环境变量中手动配置的。例如,当您在客户端上指定 Squid 服务器的 IP 地址和端口 3128 时。
- 透明代理:客户端上不需要代理设置。“代理”角色对客户端是透明的。例如,企业网络上的 Web 网关设备是透明代理。
分类依据:代理是否加密HTTPS
- 隧道代理:这是一个透明传输流量的代理。代理服务器专门通过 TCP 透明地传输 HTTPS 流量。它不会解密或感知其代理流量的特定内容。客户端与目标服务器执行直接 TLS/SSL 交互。本文介绍了与此类型相关的NGINX代理模式。
- 中间人 (MITM) 代理:代理服务器解密 HTTPS 流量,使用自签名证书完成与客户端的 TLS/SSL 握手,并完成与目标服务器的正常 TLS 交互。在客户端-代理-服务器链接上设置了两个 TLS/SSL 会话。
注意:在这种情况下,客户端在TLS握手过程中实际获取了代理服务器的自签名证书,默认情况下证书链的验证不成功。代理自签名证书中的根 CA 证书必须在客户端上受信任。因此,客户端知道此过程中的代理。如果将自签名根 CA 证书推送到客户端(在企业的内部环境中实现),则可实现透明代理。
转发代理处理 HTTPS 流量时需要特殊处理
在充当反向代理时,代理服务器通常会终止 HTTPS 加密流量并将其转发到后端实例。HTTPS 流量的加密、解密和身份验证发生在客户端和反向代理服务器之间。
另一方面,当充当转发代理并处理客户端发送的流量时,代理服务器不会在客户端请求的URL中看到目标域名,因为HTTP流量已加密并封装在TLS / SSL中,如下图所示。因此,与 HTTP 流量不同,HTTPS 流量在代理实现期间需要一些特殊处理。
NGINX解决方案
根据前面几节中的分类,当NGINX用作HTTPS代理时,代理是透明的传输(隧道)代理,既不解密也不感知上层流量。具体来说,有两种NGINX解决方案可用:第7层(L7)和第4层(L4)。以下各节详细介绍了这些解决方案。
HTTP 连接隧道 (L7 解决方案)
历史背景
早在1998年TLS还没有正式上市的时候,推广SSL协议的网景就提出使用Web代理进行SSL流量的隧道。核心思想是使用HTTP CONNECT请求在客户端和代理之间建立HTTP CONNECT隧道。CONNECT 请求必须指定客户端需要访问的目标主机和端口。互联网草案中的原始图表如下:
有关整个过程的详细信息,请参阅 HTTP:权威指南中的图表。以下步骤简要概述了该过程。
1)?客户端向代理服务器发送 HTTP CONNECT 请求。 2)?代理服务器使用 HTTP CONNECT 请求中的主机和端口信息与目标服务器建立 TCP 连接。 3)?代理服务器向客户端返回 HTTP 200 响应。 4)?客户端与代理服务器建立 HTTP CONNECT 隧道。在 HTTPS 流量到达代理服务器后,代理服务器通过 TCP 连接透明地将 HTTPS 流量传输到远程目标服务器。代理服务器仅透明地传输 HTTPS 流量,不解密 HTTPS 流量。
ngx_http_proxy_connect_module
作为反向代理服务器,NGINX并不正式支持HTTP CONNECT方法。但是,由于NGINX的模块化和可扩展功能,阿里巴巴@chobits提供了连接模块(中文内容)来支持HTTP CONNECT方法,以扩展NGINX作为转发代理。ngx_http_proxy_connect_module
环境建设
以 CentOS 7 環境為例,讓我們詳細看看這過程。
1) 环境安装
对于新环境安装,请参阅安装连接模块的常见安装步骤(中文内容)。ngx_http_proxy_connect_module
安装相应版本的修补程序,并在“configure”命令下添加参数,如以下示例所示。--add-module=/path/to/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 \
--with-threads \
--add-module=/root/src/ngx_http_proxy_connect_module
此外,为现有环境添加,如下所示。ngx_http_proxy_connect_module
# 停止NGINX服务
# systemctl stop nginx
# 备份原执行文件
# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
# 在源代码路径重新编译
# cd /usr/local/src/nginx-1.16.0
./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--add-module=/root/src/ngx_http_proxy_connect_module
# make
# 不要make install
# 将新生成的可执行文件拷贝覆盖原来的nginx执行文件
# cp objs/nginx /usr/local/nginx/sbin/nginx
# /usr/bin/nginx -V
nginx version: nginx/1.16.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
built with OpenSSL 1.0.2k-fips 26 Jan 2017
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-threads --add-module=/root/src/ngx_http_proxy_connect_module
2) 配置 nginx.conf 文件
执行以下命令以配置文件。nginx.conf
server {
listen 443;
# dns resolver used by forward proxying
resolver 114.114.114.114;
# forward proxy for CONNECT request
proxy_connect;
proxy_connect_allow 443;
proxy_connect_connect_timeout 10s;
proxy_connect_read_timeout 10s;
proxy_connect_send_timeout 10s;
# forward proxy for non-CONNECT request
location / {
proxy_pass http://$host;
proxy_set_header Host $host;
}
}
应用场景
在 L7 解决方案中,HTTP CONNECT 请求必须建立隧道,因此,代理服务器是客户端必须感知的公共代理。在客户端上手动配置 HTTP(S) 代理服务器的 IP 地址和端口。使用 cURL 的“-x”参数访问客户端,如下所示。
# curl https://www.baidu.com -svo /dev/null -x 39.105.196.164:443
* About to connect() to proxy 39.105.196.164 port 443 (#0)
* Trying 39.105.196.164...
* Connected to 39.105.196.164 (39.105.196.164) port 443 (#0)
* Establish HTTP proxy tunnel to www.baidu.com:443
> CONNECT www.baidu.com:443 HTTP/1.1
> Host: www.baidu.com:443
> User-Agent: curl/7.29.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 Connection Established
< Proxy-agent: nginx
<
* Proxy replied OK to CONNECT request
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
CApath: none
* SSL connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate:
* subject: CN=baidu.com,O="Beijing Baidu Netcom Science Technology Co., Ltd",OU=service operation department,L=beijing,ST=beijing,C=CN
...
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: www.baidu.com
> Accept: */*
>
< HTTP/1.1 200 OK
...
{ [data not shown]
由“-v”参数打印的上述详细信息指示客户端首先与代理服务器 39.105.196.164 建立 HTTP CONNECT 隧道。一旦代理回复为“HTTP/1.1 200 连接已建立”,客户端就会启动 TLS/SSL 握手并将流量发送到服务器。
NGINX流(L4解决方案)
由于上层流量是透明传输的,因此这里出现的关键问题是NGINX是否应该充当“L4代理”来实现TCP / UDP以上协议的完全透明传输。答案是肯定的。NGINX 1.9.0或更高版本支持ngx_stream_core_module。默认情况下不生成此模块。在命令下添加选项以启用此模块。-- with-stream configure
常见问题
使用NGINX流作为TCP层HTTPS流量的代理,会导致本文开头提到的相同问题:代理服务器未获得客户端要访问的目标域名。发生这种情况是因为在 TCP 层获得的信息仅限于 IP 地址和端口,而不获取域名。要获取目标域名,代理必须能够从上层数据包中提取域名。因此,NGINX流不是严格意义上的L4代理,它必须寻求上层的帮助才能提取域名。
ngx_stream_ssl_preread_module
为了在不解密HTTPS流量的情况下获取HTTPS流量的目标域名,唯一的方法是在TLS/SSL握手期间使用第一个ClientHello数据包中包含的SNI字段。从版本1.11.5开始,NGINX支持ngx_stream_ssl_preread_module。此模块有助于从 ClientHello 数据包获取 SNI 和 ALPN。对于L4转发代理,从ClientHello数据包中提取SNI的能力至关重要,否则NGINX流解决方案将无法实现。但是,这也带来了一个限制,即在 TLS/SSL 握手期间,所有客户端都必须在 ClientHello 数据包中包含 SNI 字段。否则,NGINX流代理将不知道客户端需要访问的目标域名。
环境建设
1) 环境安装
对于新安装的环境,请参考常用安装步骤(中文内容),直接在“configure”命令下添加、、和选项。请考虑以下示例以更好地理解。--with-stream --with-stream_ssl_preread_module --with-stream_ssl_module
./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--with-stream \
--with-stream_ssl_preread_module \
--with-stream_ssl_module
为已安装和已编译的环境添加上述三个与流相关的模块,如下所示。
# 停止NGINX服务
# systemctl stop nginx
# 备份原执行文件
# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
# 在源代码路径重新编译
# cd /usr/local/src/nginx-1.16.0
# ./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--with-stream \
--with-stream_ssl_preread_module \
--with-stream_ssl_module
# make
# 不要make install
# 将新生成的可执行文件拷贝覆盖原来的nginx执行文件
# cp objs/nginx /usr/local/nginx/sbin/nginx
# nginx -V
nginx version: nginx/1.16.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
built with OpenSSL 1.0.2k-fips 26 Jan 2017
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-threads --with-stream --with-stream_ssl_preread_module --with-stream_ssl_module
2) 配置 nginx.conf 文件
与HTTP不同,在流块中配置NGINX流。但是,命令参数与HTTP块的命令参数类似。以下代码段显示了主配置。
stream {
resolver 114.114.114.114;
server {
listen 443;
ssl_preread on;
proxy_connect_timeout 5s;
proxy_pass $ssl_preread_server_name:$server_port;
}
}
应用场景
作为L4转发代理,NGINX基本上透明地将流量传输到上层,并且不需要HTTP CONNECT来建立隧道。因此,L4 解决方案适用于透明代理模式。例如,当目标域名通过 DNS 解析定向到代理服务器时,需要通过绑定到客户端来模拟透明代理模式。/etc/hosts
以下代码段显示了客户端上的命令:
cat /etc/hosts
...
# 把域名www.baidu.com绑定到正向代理服务器39.105.196.164
39.105.196.164 www.baidu.com
# 正常利用curl来访问www.baidu.com即可。
# curl https://www.baidu.com -svo /dev/null
* About to connect() to www.baidu.com port 443 (#0)
* Trying 39.105.196.164...
* Connected to www.baidu.com (39.105.196.164) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
CApath: none
* SSL connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate:
* subject: CN=baidu.com,O="Beijing Baidu Netcom Science Technology Co., Ltd",OU=service operation department,L=beijing,ST=beijing,C=CN
* start date: 5月 09 01:22:02 2019 GMT
* expire date: 6月 25 05:31:02 2020 GMT
* common name: baidu.com
* issuer: CN=GlobalSign Organization Validation CA - SHA256 - G2,O=GlobalSign nv-sa,C=BE
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: www.baidu.com
> Accept: */*
>
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
< Connection: Keep-Alive
< Content-Length: 2443
< Content-Type: text/html
< Date: Fri, 21 Jun 2019 05:46:07 GMT
< Etag: "5886041d-98b"
< Last-Modified: Mon, 23 Jan 2017 13:24:45 GMT
< Pragma: no-cache
< Server: bfe/1.0.8.18
< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
<
{ [data not shown]
* Connection #0 to host www.baidu.com left intact
常见问题
现在,让我们快速看一下有关L4解决方案的关键问题。
1)?由于客户端上的手动代理设置,访问尝试失败。
L4 转发代理透明地传输上层 HTTPS 流量,不需要 HTTP CONNECT 即可建立隧道。因此,没有必要在客户端上设置 HTTP(S) 代理。关键问题是,在客户端上手动设置 HTTP(S) 代理是否可确保访问尝试成功。使用 cURL 的“-x”参数设置转发代理服务器并测试对此服务器的访问。以下代码段显示了结果。
# curl https://www.baidu.com -svo /dev/null -x 39.105.196.164:443
* About to connect() to proxy 39.105.196.164 port 443 (#0)
* Trying 39.105.196.164...
* Connected to 39.105.196.164 (39.105.196.164) port 443 (#0)
* Establish HTTP proxy tunnel to www.baidu.com:443
> CONNECT www.baidu.com:443 HTTP/1.1
> Host: www.baidu.com:443
> User-Agent: curl/7.29.0
> Proxy-Connection: Keep-Alive
>
* Proxy CONNECT aborted
* Connection #0 to host 39.105.196.164 left intact
结果指示客户端尝试在 NGINX 之前建立 HTTP CONNECT 隧道。但是,由于NGINX透明地传输流量,因此CONNECT请求直接转发到目标服务器。目标服务器不接受 CONNECT 方法。因此,“代理连接中止”反映在上面的代码段中,导致访问失败。
2)?访问尝试失败,因为客户端在 ClientHello 数据包中不包含 SNI。
如前所述,当NGINX流用作转发代理时,使用从ClientHello中提取SNI字段至关重要。如果客户端在 ClientHello 数据包中未包含 SNI,则代理服务器将不知道目标域名,从而导致访问失败。ngx_stream_ssl_preread_module
在透明代理模式(由手动绑定主机模拟)下,使用 OpenSSL 在客户端上进行模拟。
# openssl s_client -connect www.baidu.com:443 -msg
CONNECTED(00000003)
>>> TLS 1.2 [length 0005]
16 03 01 01 1c
>>> TLS 1.2 Handshake [length 011c], ClientHello
01 00 01 18 03 03 6b 2e 75 86 52 6c d5 a5 80 d7
a4 61 65 6d 72 53 33 fb 33 f0 43 a3 aa c2 4a e3
47 84 9f 69 8b d6 00 00 ac c0 30 c0 2c c0 28 c0
24 c0 14 c0 0a 00 a5 00 a3 00 a1 00 9f 00 6b 00
6a 00 69 00 68 00 39 00 38 00 37 00 36 00 88 00
87 00 86 00 85 c0 32 c0 2e c0 2a c0 26 c0 0f c0
05 00 9d 00 3d 00 35 00 84 c0 2f c0 2b c0 27 c0
23 c0 13 c0 09 00 a4 00 a2 00 a0 00 9e 00 67 00
40 00 3f 00 3e 00 33 00 32 00 31 00 30 00 9a 00
99 00 98 00 97 00 45 00 44 00 43 00 42 c0 31 c0
2d c0 29 c0 25 c0 0e c0 04 00 9c 00 3c 00 2f 00
96 00 41 c0 12 c0 08 00 16 00 13 00 10 00 0d c0
0d c0 03 00 0a 00 07 c0 11 c0 07 c0 0c c0 02 00
05 00 04 00 ff 01 00 00 43 00 0b 00 04 03 00 01
02 00 0a 00 0a 00 08 00 17 00 19 00 18 00 16 00
23 00 00 00 0d 00 20 00 1e 06 01 06 02 06 03 05
01 05 02 05 03 04 01 04 02 04 03 03 01 03 02 03
03 02 01 02 02 02 03 00 0f 00 01 01
140285606590352:error:140790E5:SSL routines:ssl23_write:ssl handshake failure:s23_lib.c:177:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 289 bytes
?
默认情况下,OpenSSL 不包括 SNI。如代码段所示,在发送 ClientHello 后,前面的请求将在 TLS/SSL 握手阶段终止。发生这种情况是因为代理服务器不知道应将 ClientHello 转发到的目标域名。s_client
使用带有“服务器名称”参数的 OpenSSL 来指定 SNI,将产生成功的访问。
# openssl s_client -connect www.baidu.com:443 -servername www.baidu.com
结论
本文介绍了使用NGINX作为HTTPS流量转发代理的两种方法。它总结了NGINX使用HTTP CONNECT隧道和NGINX流充当HTTPS转发代理的解决方案的原则,环境构建要求,应用场景和关键问题。本文在各种情况下使用NGINX作为转发代理时用作参考,具体情况参考www.87cloud.com
|