写点什么

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

2019 年 7 月 04 日

使用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: 509 01:22:02 2019 GMT* expire date: 625 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 年 7 月 04 日 08:0027225

评论 1 条评论

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

架构师训练营第 1 期 -week2

习习

架构师训练营 - 第 2 周学习总结(1 期)

阿甘

救人于无形的“环境智能”,到底是一种什么智能?

脑极体

时空碰撞优化系列·二

誓约·追光者

hive Sparksql 计算效率 优化

LeetCode题解:145. 二叉树的后序遍历,递归,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

Redis 发布订阅,小功能大用处,真没那么废材!

楼下小黑哥

Java redis spring

微前端qiankun从搭建到部署的实践

fengxianqi

前端框架 微前端 微应用

看动画学算法之:排序-count排序

程序那些事

动画 看动画学算法 看动画学数据结构 count排序

架构师训练营第 1 期 第 1 周作业

李循律

甲方日常 20

句子

工作 随笔杂谈 日常 Java 25 周年

船长梁晓玲的猎鹰号真的能赚钱嘛?不分析不知道……

成周

心理学 船长梁晓玲 诈骗

我把某大厂P8大牛手写的 Linux+网络编程 手册搞到手了

互联网架构师小马

Java Linux 程序员 网络编程 操作系统

高并发下为什么更喜欢进程内缓存

架构师修行之路

缓存 架构设计

分布式高并发下Actor模型如此优秀

架构师修行之路

系统设计 reactor 高并发

第7周的总结

Vincent

极客时间 极客大学

一个草根的日常杂碎(9月22日)

刘新吾

生活 随笔 记录

架构师训练营 1 期第 2 周:框架设计 - 作业

piercebn

极客大学架构师训练营

架构师训练营 - 第 2 周课后作业(1 期)

阿甘

响应式编程到底是什么?

博文视点Broadview

Java 响应式 响应式编程 reactor 并发

c++杂谈-1

菜鸟小sailor 🐕

c++

Spring系列之新注解配置+Spring集成junit+注解注入

云流

Java spring 架构师 微服务框架

网站日志分析最完整实践

MySQL从删库到跑路

“大数据+区块链”的智慧城市建设!

CECBC区块链专委会

区块链 大数据

添加字幕哪个视频剪辑软件比较简单?

奈奈的杂社

视频创作 视频剪辑 视频后期 自媒体 后期字幕

众盟科技2020智能化白皮书:穿越新商业周期,读懂商业智能化的真义

脑极体

知乎万赞,获得腾讯offer后自述,编程能力是如何突飞猛进的

周老师

Java 编程 程序员 架构 面试

一夜爆火,只因阿里内部作为参考的SpringBoot巅峰之作git开源

小Q

Java 架构 面试 微服务 springboot

第7周作业

Vincent

极客时间 极客大学

oeasy 教您玩转 linux 010400 总结 summary

o

时空碰撞优化系列·一

誓约·追光者

hive 数据分析 Sparksql 计算效率 优化

判断一个请求是否是Ajax异步请求

麦叔

ajax

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