NGINX主要設計做爲反向代理服務器,但隨着NGINX的發展,它一樣能做爲正向代理的選項之一。正向代理自己並不複雜,而如何代理加密的HTTPS流量是正向代理須要解決的主要問題。本文將介紹利用NGINX來正向代理HTTPS流量兩種方案,及其使用場景和主要問題。html
簡單介紹下正向代理的分類做爲理解下文的背景知識:nginx
做爲反向代理時,代理服務器一般終結 (terminate) HTTPS加密流量,再轉發給後端實例。HTTPS流量的加解密和認證過程發生在客戶端和反向代理服務器之間。git
而做爲正向代理在處理客戶端發過來的流量時,HTTP加密封裝在了TLS/SSL中,代理服務器沒法看到客戶端請求URL中想要訪問的域名,以下圖。因此代理HTTPS流量,相比於HTTP,須要作一些特殊處理。github
根據前文中的分類方式,NGINX解決HTTPS代理的方式都屬於透傳(隧道)模式,即不解密不感知上層流量。具體的方式有以下7層和4層的兩類解決方案。web
早在1998年,也就是TLS尚未正式誕生的SSL時代,主導SSL協議的Netscape公司就提出了關於利用web代理來tunneling SSL流量的INTERNET-DRAFT。其核心思想就是利用HTTP CONNECT請求在客戶端和代理之間創建一個HTTP CONNECT Tunnel,在CONNECT請求中須要指定客戶端須要訪問的目的主機和端口。Draft中的原圖以下:sql
整個過程能夠參考HTTP權威指南中的圖:後端
NGINX做爲反向代理服務器,官方一直沒有支持HTTP CONNECT方法。可是基於NGINX的模塊化、可擴展性好的特性,阿里的@chobits提供了ngx_http_proxy_connect_module模塊,來支持HTTP CONNECT方法,從而讓NGINX能夠擴展爲正向代理。瀏覽器
以CentOS 7的環境爲例。服務器
1) 安裝
對於新安裝的環境,參考正常的安裝步驟和安裝這個模塊的步驟,把對應版本的patch打上以後,在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
對於已經安裝編譯安裝完的環境,須要加入以上模塊,步驟以下:
# 中止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文件配置
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 Connection Established後就開始交互TLS/SSL握手和流量了。
既然是使用透傳上層流量的方法,那可不可作成「4層代理」,對TCP/UDP以上的協議實現完全的透傳呢?答案是能夠的。NGINX官方從1.9.0版本開始支持ngx_stream_core_module模塊,模塊默認不build,須要configure時加上--with-stream選項來開啓。
用NGINX stream在TCP層面上代理HTTPS流量確定會遇到本文一開始提到的那個問題:代理服務器沒法獲取客戶端想要訪問的目的域名。由於在TCP的層面獲取的信息僅限於IP和端口層面,沒有任何機會拿到域名信息。要拿到目的域名,必需要有拆上層報文獲取域名信息的能力,因此NGINX stream的方式不是徹底嚴格意義上的4層代理,仍是要略微藉助些上層能力。
要在不解密的狀況下拿到HTTPS流量訪問的域名,只有利用TLS/SSL握手的第一個Client Hello報文中的擴展地址SNI (Server Name Indication)來獲取。NGINX官方從1.11.5版本開始支持利用ngx_stream_ssl_preread_module模塊來得到這個能力,模塊主要用於獲取Client Hello報文中的SNI和ALPN信息。對於4層正向代理來講,從Client 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個與stream相關的模塊,步驟以下:
# 中止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文件配置
NGINX stream與HTTP不一樣,須要在stream塊中進行配置,可是指令參數與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.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
1) 客戶端手動設置代理致使訪問不成功
4層正向代理是透傳上層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 tunnel,可是因爲NGINX是透傳,因此把CONNECT請求直接轉發給了目的服務器。目的服務器不接受CONNECT方法,因此最終出現"Proxy CONNECT aborted",致使訪問不成功。
2) 客戶端沒有帶SNI致使訪問不成功
上文提到用NGINX stream作正向代理的關鍵因素之一是利用ngx_stream_ssl_preread_module提取出Client Hello中的SNI字段。若是客戶端客戶端不攜帶SNI字段,會形成代理服務器沒法獲知目的域名的狀況,致使訪問不成功。
在透明代理模式下(用手動綁定hosts的方式模擬),咱們能夠在客戶端用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 s_client默認不帶SNI,能夠看到上面的請求在TLS/SSL握手階段,發出Client Hello後就結束了。由於代理服務器不知道要把Client Hello往哪一個目的域名轉發。
若是用openssl帶servername參數來指定SNI,則能夠正常訪問成功,命令以下:
# openssl s_client -connect www.baidu.com:443 -servername www.baidu.com
本文總結了NGINX利用HTTP CONNECT隧道和NGINX stream兩種方式作HTTPS正向代理的原理,環境搭建,使用場景和主要問題,但願給你們在作各類場景的正向代理時提供參考。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。