当前位置: 首页 > 科技观察

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

时间:2023-03-13 06:06:25 科技观察

NGINX主要是作为反向代理服务器设计的,但是随着NGINX的发展,它也可以作为正向代理选项之一。正向代理本身并不复杂,但是如何代理加密的HTTPS流量才是正向代理需要解决的主要问题。本文将介绍两种使用NGINX转发代理HTTPS流量的方案、使用场景和主要问题。HTTP/HTTPS正向代理的分类简单介绍一下正向代理的分类,作为理解以下内容的背景知识:根据客户端是否感知分类普通代理:客户端需要在浏览器或系统环境变量中手动设置代理地址和端口。比如squid,在客户端指定squid服务器IP和3128端口。透明代理:客户端不需要做任何代理设置,“代理”的作用对客户端是透明的。例如,企业网络链路中的WebGateway设备。根据proxy是否解密HTTPSTunnelproxy分类:即透明代理。代理服务器只透传TCP协议上的HTTPS流量,不会解密和感知代理流量的具体内容。客户端与其访问的目标服务器执行直接的TLS/SSL交互。本文中讨论的NGINX代理方法属于这种模式。中间人(MITM,Man-in-the-Middle)代理:代理服务器解密HTTPS流量,使用自签名证书完成与客户端的TLS/SSL握手,与客户端完成正常的TLS交互目标服务器。在客户端-代理-服务器链接中建立两个TLS/SSL会话。喜欢Charles的可以参考文章简单的原理说明。https://www.jianshu.com/p/405f9d76f8c4注意:这种情况下,客户端实际上是在TLS握手阶段获取了代理服务器自己的自签名证书。证书链的校验默认失败,需要在客户端校验。信任代理自签名证书的根CA证书。所以客户感受到了这个过程。如果要做无意义的透明代理,需要将自建的RootCA证书推送到客户端,这在企业内部环境下是可以实现的。为什么转发代理需要对HTTPS流量进行特殊处理?当作为反向代理时,代理服务器通常会在将其转发到后端实例之前终止(终止)HTTPS加密流量。HTTPS流量的加密、解密和认证过程发生在客户端和反向代理服务器之间。作为正向代理,在处理客户端发送的流量时,HTTP加密封装在TLS/SSL中,代理服务器在客户端请求的URL中看不到客户端要访问的域名,如图下图。因此,与HTTP相比,代理HTTPS流量需要一些特殊的处理。NGINX的解决方案按照前文的分类方式,NGINX解决HTTPS代理的方式属于透传(隧道)模式,即不解密,不感知上层流量。具体方式包括以下两类方案:7层和4层。HTTPCONNECT隧道(7层解决方案)历史背景早在1998年,也就是TLS正式诞生之前的SSL时代,主导SSL协议的Netscape就提出了关于使用web代理对SSL流量进行隧道传输的INTERNET-DRAFT。它的核心思想是利用HTTPCONNECT请求在客户端和代理之间建立一个HTTPCONNECTTunnel。在CONNECT请求中,需要指定客户端需要访问的目的主机和端口。Draft中原图如下:整个过程可以参考HTTP权威指南中的图片:客户端向代理服务器发送HTTPCONNECT请求。代理服务器使用HTTPCONNECT请求中的主机和端口与目标服务器建立TCP连接。代理服务器向客户端返回HTTP200响应。客户端和代理服务器建立一个HTTPCONNECT隧道。HTTPS流量到达代理服务器后,通过TCP透传到远程目的服务器。代理服务器的作用是透传HTTPS流量,HTTPS不需要解密。NGINXngx_http_proxy_connect_module模块NGINX作为反向代理服务器,官方已经不支持HTTPCONNECT方式。但是基于NGINX的模块化和可扩展性,阿里的@chobits提供了ngx_http_proxy_connect_module模块来支持HTTPCONNECT方式,使得NGINX可以扩展为正向代理。环境搭建以CentOS7环境为例。1)安装对于新安装的环境,参考正常安装步骤和安装本模块的步骤(https://github.com/chobits/ngx_http_proxy_connect_module),对应版本打补丁后,添加参数--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对于已经安装编译安装的环境,需要添加以上模块。步骤如下:#停止NGINX服务#systemctlstopginx#备份原执行文件#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#不要makeinstall#复制新生成的可执行文件并覆盖原来的nginx可执行文件#cpobjs/nginx/usr/local/nginx/sbin/nginx#/usr/bin/nginx-Vnginxversion:nginx/1.16.0builtbygcc4.8.520150623(RedHat4.8.5-36)(GCC)builtwithOpenSSL1.0.2k-fips26Jan2017TLSSNI支持启用配置参数:--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_module2)nginx.conf文件配置server{listen443;#dnsresolverusedbyforwardproxyingresolver114.114.114.114;#forwardproxyforCONNECTrequestproxy_connect;proxy_connect_allow443;proxy_connect_connect_timeout10s;proxy_connect_read_timeout10s;proxy_connect_send_timeout10s;#forwardproxyfornon-CONNECTrequestlocation/{proxy_passhttp://$host;proxy_set_headerHost$host;}}使用场景Layer7需要通过HTTPCONNECT建立隧道,这是一种常见的客户端感知的代理方式。需要在客户端手动配置HTTP(S)代理服务器的IP和端口在客户端使用curl加-x参数访问如下:#curlhttps://www.baidu.com-svo/dev/null-x39.105.196.164:443*Abouttoconnect()toproxy39.105.196.164port443(#0)*Trying39.105.196.164...*Connectedto39.105.196.164(39.105.196.164)port443(#0)*EstablishHTTPproxytunneltowww.baidu.com:443>CONNECTwww.baidu.com:443HTTP/1.1>Host:www.baidu.com:443>User-Agent:curl/7.29.0>Proxy-Connection:Keep-Alive>GET/HTTP/1.1>User-Agent:curl/7.29.0>Host:www.baidu.com>Accept:*/*>GET/HTTP/1.1>User-Agent:curl/7.29.0>Host:www.baidu.com>Accept:*/*>CONNECTwww.baidu.com:443HTTP/1.1>Host:www.baidu.com:443>User-Agent:curl/7.29.0>Proxy-Connection:Keep-Alive>*ProxyCONNECTaborted*Connection#0tohost39.105.196.164leftintact可以看到客户端在转发给NGINX之前是尝试建立HTTPCONNECT隧道,但是由于NGINX是透明的,所以CONNECT请求直接转发给了目标服务器。目的服务器不接受CONNECT方法,所以最后出现“ProxyCONNECTaborted”,导致访问不成功。2)客户端没有SNI,导致访问不成功。如上所述,使用NGINX流作为正向代理的关键因素之一是使用ngx_stream_ssl_preread_module来提取ClientHello中的SNI字段。如果客户端没有携带SNI字段,代理服务器将无法获取到目的域名,导致访问不成功。在透明代理模式下(通过手动绑定hosts来模拟),我们可以在客户端使用openssl来模拟:#openssls_client-connectwww.baidu.com:443-msgCONNECTED(00000003)>>>TLS1.2[length0005]160301011c>>>TLS1.2Handshake[length011c],ClientHello0100011803036b2e7586526cd5a580d7a461656d725333fb33f043a3aac24ae347849f698bd60000acc030c02cc028c024c014c00a00a500a300a1009f006b006a0069006800390038003700360088008700860085c032c02ec02ac026c00fc005009d003d00350084c02fc02bc027c023c013c00900a400a200a0009e00670040003f003e0033003200310030009a0099009800970045004400430042c031c02dc029c025c00ec004009c003c002f00960041c012c008001600130010000dc00dc003000a0007c011c007c00cc0020005000400ff01000043000b000403000102000a000a0008001700190018001600230000000d0020001e060106020603050105020503040104020403030103020303020102020203000f000101140285606590352:error:140790E5:SSLroutines:ssl23_write:sslhandshakefailure:s23_lib.c:177:---nopeercertificateavailable---NoclientcertificateCAnamessent---SSLhandshakehasread0bytesandw里顿289bytes...openssls_client默认不包含SNI。可以看到上面的请求是在TLS/SSL握手阶段,发送完ClientHello就结束了,因为代理服务器不知道要把ClientHello转发到哪个目的域名。如果使用openssl加上servername参数指定SNI,可以正常访问,命令如下:#openssls_client-connectwww.baidu.com:443-servernamewww.baidu.com总结本文总结了NGINX使用HTTPCONNECT的两种方式tunnel和NGINXstreamHTTPS正向代理的原理、环境搭建、使用场景和主要问题,希望能为大家做各种场景的正向代理时提供参考。作者:怀志