11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

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

  • 2019-07-04
  • 本文字数:6758 字

    阅读完需:约 22 分钟

使用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 \--with-threads \--add-module=/root/src/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 -Vnginx version: nginx/1.16.0built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)built with OpenSSL 1.0.2k-fips  26 Jan 2017TLS SNI support enabledconfigure 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 文件配置


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; } }
复制代码


使用场景

7 层需要通过 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 流(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 选项即可。示例如下:


./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
复制代码


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


# 停止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 -Vnginx version: nginx/1.16.0built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)built with OpenSSL 1.0.2k-fips  26 Jan 2017TLS SNI support enabledconfigure 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 文件配置


NGINX stream 与 HTTP 不同,需要在流块中进行配置,但是指令参数与 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;    }}
复制代码


使用场景

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


在客户端:


cat /etc/hosts...# 把域名www.baidu.com绑定到正向代理服务器39.105.196.16439.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
复制代码


常见问题

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


4 层正向代理是透传上层 HTTPS 流量,不需要 HTTP 连接来建立隧道,也就是说不需要客户端设置 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 方法,所以最终出现“Proxy CONNECT aborted”,导致访问不成功。


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


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


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


# openssl s_client -connect www.baidu.com:443 -msgCONNECTED(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 01140285606590352: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 s_client 默认不带 SNI,可以看到上面的请求在 TLS / SSL 握手阶段,发出客户端 Hello 后就结束了。因为代理服务器不知道要把客户端 Hello 往哪个目的域名转发。


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


# openssl s_client -connect www.baidu.com:443 -servername www.baidu.com
复制代码


总结

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


本文转载自云栖社区


原文链接


https://yq.aliyun.com/articles/706196?spm=a2c4e.11157919.spm-cont-list.82.146cf204yvK0Xp


2019-07-04 08:0039618

评论 1 条评论

发布
用户头像
非常感谢分享。
2020-06-30 22:35
回复
没有更多了
发现更多内容

作业 - 第四周

eva

作业 - 第四章 业务流程与产品文档 (一)

hao hao

别困惑,不是你的错!90%的开发者把Clubhouse看成了Clickhouse

京东科技开发者

Clickhouse 社交 clubhouse

架构的变迁,从分层架构先聊起

华为云开发者联盟

架构 软件 分层架构 架构师 系统

惊呆,一条sql竟然让oracle奔溃了

君哥聊技术

oracle mybatis 批量操作

产品0期 - 第四周作业

曾烧麦

产品训练营

一个只会写Bug的Coder年终总结

z小赵

程序员 互联网 职场成长

产品经理训练营作业 03

KingSwim

WEEK4作业

Geek_6a8931

极客时间APP购买课程模块用例文档

夏天的风

用例图

技术文档丨循迹搭建--车辆集成

百度开发者中心

【百度官方技术分享】中间件技术在百度云原生测试中的应用实践

百度Geek说

产品 架构 测试 中间件 技术宅

话题讨论 | 你选择去一线城市还是老家的省会城市?

石云升

话题讨论 职业发展 2月春节不断更

红信圈系统开发,红信圈APP开发

luluhulian

【新春特辑】发压岁钱、看贺岁片、AI写春联……华为云社区给大家拜年了

华为云开发者联盟

华为云

我认为的互联网医疗场景用户及场景

卢嘉敏

需求 医疗 用户

2021年人工智能数据采集标注行业四大趋势预测;清华提出深度对齐聚类用于新意图发现

京东科技开发者

人工智能 数字货币

产品0期 - 第四周作业 - 附件1

曾烧麦

产品训练营

关于产品文档与原型的思考

【STM32】GPIO输入—按键检测

AXYZdong

硬件 stm32 2月春节不断更

用例图

Eva

第四周 开启新的篇章,打磨产品的最强辅助——文档

小匚

极客时间 产品经理 产品经理训练营

有了这个算法,图像上文字擦除再也用不上PS了

华为云开发者联盟

深度学习 算法 GAN 文字擦除 图像

交易所搭建

v16629866266

交易所开发

第四次作业

Geek_79e983

在游戏运营行业,Serverless 如何解决数据采集分析痛点?

阿里巴巴云原生

Serverless 运维 云原生 关系型数据库 消息中间件

流媒体传输协议之 RTP (上篇)

阿里云视频云

音视频 流媒体 rtp

互联网医疗里,用户需要的是什么

卢嘉敏

需求 医疗 用户

当自动驾驶遇到5G,会擦出怎样的火花?这篇文章说明白了

华为云开发者联盟

人工智能 自动驾驶 AI 5G

Vue开发中可以使用的ES6新特征

devpoint

Vue ES6

京东科技集团21篇论文高票入选国际顶会AAAI 2021

京东科技开发者

机器学习 AI

使用NGINX作为HTTPS正向代理服务器_技术管理_怀知_InfoQ精选文章