使用 Nginx 搭建 HTTPS 正向代理服务

原创文章,如需转载,请注明来自:https://bigzuo.github.io/

NGINX 搭建 HTTP 正向代理

最近帮同事搭建一个代理服务器,要求当请求的请求头中包含dest_ip时,就将请求转发到这个目的地址,否则就正常请求。当自己用下面这种方式很快就实现 HTTP 正向代理,信心满满的交给同事使用时,却发现这种配置无法正常代理 HTTPS 请求。

1
2
3
4
5
6
location / {
if ($http_dest_ip != "") {
proxy_pass http://$http_dest_ip/$request_uri;
}
proxy_pass https://$http_host$request_uri;
}

NGINX 代理 HTTPS 请求时 access 日志:

1
2
3
192.168.73.26 - - [06/Dec/2018:19:42:27 +0800] "CONNECT acs.m.taobao.com:443 HTTP/1.1" 400 179 "-" "-" "-"
192.168.73.26 - - [06/Dec/2018:19:42:27 +0800] "CONNECT acs.m.taobao.com:443 HTTP/1.1" 400 179 "-" "-" "-"
192.168.73.26 - - [06/Dec/2018:19:42:27 +0800] "CONNECT acs.m.taobao.com:443 HTTP/1.1" 400 179 "-" "-" "-"

NGINX 的error 日志:

1
2
3
2018/12/06 19:42:27 [info] 79953#1783043: *16 client sent invalid request while reading client request line, client: 192.168.73.26, server: localhost, request: "CONNECT gw.alicdn.com:443 HTTP/1.1"
2018/12/06 19:42:27 [info] 79953#1783043: *17 client sent invalid request while reading client request line, client: 192.168.73.26, server: localhost, request: "CONNECT gw.alicdn.com:443 HTTP/1.1"
2018/12/06 19:42:27 [info] 79953#1783043: *18 client sent invalid request while reading client request line, client: 192.168.73.26, server: localhost, request: "CONNECT acs.m.taobao.com:443 HTTP/1.1"

为什么 NGINX 不能做 HTTPS 正向代理服务器

HTTPS 现在已经被大范围的使用在网络数据安全传输领域,基于 HTTPS 的浏览器和服务器之间通信都是被加密的。所以,当浏览器通过代理发送一个 HTTPS 请求时,请求的地址和端口也是被加密的,代理服务器也无法知道这些信息。那么代理是如何知道请求是发到哪里呢?为了解决这个问题,浏览器会先发送一个明文的 HTTP 协议的 CONNECT 请求给代理服务器,告诉代理请求的目的地址和端口。CONNECT 请求的内容格式如下:

1
2
3
4
CONNECT ***:443 HTTP/1.1
Host: bayden.com:443
Connection: keep-alive
User-Agent: Chrome/47.0.2526.58

收到这个请求后,代理会和目标服务器建立一个 TCP 连接,并返回一个 HTTP 200 的响应给浏览器,告诉浏览器自己和目标服务器的 TCP 连接已建立。响应格式如下:

1
2
HTTP/1.1 200 Connection Established
Connection: close

之后,代理只会透明的来回传输浏览器和服务器之间经过 SSL 加密的数据包,并不知道也不需要知道传输的实际内容,直接通道关闭。

出现以上异常的具体原因是 NGINX 本身的设计就是作为一个反向代理服务器,而非正向代理服务器,并且在短期也没有打算支持正向代理,所以现在 NGINX 并不支持 CONNECT 请求方式,因此收到“CONNECT ***:443 HTTP/1.1”请求时会报“client sent invalid request while reading client request line”异常。这种情况并不是说 NGINX 无法处理 SSL,只是作为一个 forward proxy 不行。

安装扩展模块

那如何让 NGINX 可以正向代理 HTTPS 请求呢?我们需要借助一个第三方扩展模块 ngx_http_proxy_connect_module 来让 NGINX 支持 CONNECT 请求,建立一个 SSL 请求的通道。

ngx_http_proxy_connect_module 安装方式:

1
2
3
4
5
6
$ wget http://Nginx.org/download/Nginx-1.9.2.tar.gz
$ tar -xzvf Nginx-1.9.2.tar.gz
$ cd Nginx-1.9.2/
$ patch -p1 < /path/to/ngx_http_proxy_connect_module/patch/proxy_connect.patch
$ ./configure --add-module=/path/to/ngx_http_proxy_connect_module
$ make && make install

其中 “/path/to” 为 proxy_connect.patch 文件在服务器的存放地址。需要注意的是,对于使用 Mac 的同学,我目前还没有找到使用 brew install nginx 的方式安装 ngx_http_proxy_connect_module 扩展的方法。

编译安装完 ngx_http_proxy_connect_module 扩展模块后,使用如下配置即可以使 NGINX 正常代理 HTTPS 请求。

NGINX HTTPS 代理完整配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
http {
...
resolver 8.8.8.8; # DNS 服务器可根据实际情况单独配置
...

server {
listen 80;
server_name proxy_server;
...

proxy_connect;
proxy_connect_allow all;
proxy_connect_connect_timeout 10s;
proxy_connect_read_timeout 10s;
proxy_connect_send_timeout 10s;

location / {
proxy_pass http://$host;
proxy_set_header Host $host;
}

NGINX proxy for docker

当然,如果你会使用 docker,那么可以直接使用已经编译了 ngx_http_proxy_connect_module 模块的 NGINX 镜像 Nginx forward proxy 快速搭建一个 HTTPS正向代理服务器。

参考文档

ngx_http_proxy_connect_module: A forward proxy module for CONNECT request handling

Nginx 正向代理

HTTP 代理原理及实现(一)

NGINX SSL Forward Proxy Config

Understanding CONNECT Tunnels

The HTTP CONNECT tunnel

Understanding Nginx HTTP Proxying, Load、Balancing, Buffering, and Caching