搭建HTTPS Proxy

Update: 稳定使用6个多月,目前推荐使用TunSafe,方便非浏览器应用。
如确实需要HTTPS Proxy,推荐使用Caddy
安装方法:curl https://getcaddy.com | bash -s personal http.forwardproxy,不要忘记安装http.forwardproxy插件。

https://your-domain.com {
    #Your website here
    root /opt/www/html
}

http://proxy.your-domain.com:80 {
    #Used obtaining certificates only, response 404 to CONNECT request
}
https://proxy.your-domain.com:8443 {
        #Proxy only on 443
        forwardproxy {
                ports 80 443
                serve_pac
                hide_ip
                basicauth user passwd
                probe_resistance login
        }
        log stdout
}

就这么简单搭建一个支持HTTP2的代理,包括证书的申请在内。80端口为了自动申请Let's Encrypt证书,不允许代理,而代理只服务8443端口。

为什么选择8443而不是443端口是因为一个Bug导致通过代理不能访问Caddy上的其他域名,比如https://your-domain.com,所以换不同端口绕过这个问题。

其中probe_resistance的意思是只有通过代理访问http://login/才会弹出登陆框,否则断开连接,防止代理探测。login可以换成别的什么字符,使用代理时在地址栏写login/加斜杠防止进入搜索模式。
详细配置https://github.com/caddyserver/forwardproxy

以下是旧的内容,仅为理论实践,复杂而不实用,不建议尝试。


HTTPS Proxy 是指普通HTTP Proxy经过TLS加密,Chrome 和 Firefox可直连的代理,也称作HTTP over SSL Proxy。优点是无需独立客户端程序,浏览器内置支持,轻量级,支持PAC配置。缺点是服务端配置复杂,客户端只能在浏览器上使用,或者使用MEOW 或 Stunnel 去掉SSL 加密当作普通HTTP 代理。现在已经有商用的代理使用这种方案。

原理就是后端一个普通的HTTP 代理,Squid 或Tinyproxy 都可以,然后用nghttpx 或Nginx 进行反向代理和TLS加密。麻烦点在于证书的申请和配置,可以参考文末的文档。

这里使用Nginx 和Tinyproxy 的组合进行配置。系统使用Archlinux最新版,安装系统默认软件包。其他系统因为软件没有及时更新或者模块没有编译可能导致配置失败。
安装Nginx pacman -Sy nginx
检查模块 nginx -V:
其中 --with-stream --with-stream_ssl_module 是必须的。nginx相关文档
/etc/nginx/nginx.conf 配置片段:

# stream 位于配置文件最外层,与http同级
stream {
        upstream http_proxy {
                server 127.0.0.1:8888;
        }

        server {
                listen 8443 ssl;
        
                ssl_certificate /etc/letsencrypt/live/example.net/fullchain.pem;
                ssl_certificate_key /etc/letsencrypt/live/example.net/privkey.pem;
                include /etc/nginx/options-ssl-proxy.conf;
                ssl_dhparam /etc/nginx/ssl-dhparams.pem;

                proxy_pass http_proxy;
        }
}

SSL 配置由Let' Encrypt 生成,这里复制过来,修改了ssl_session_cache 避免命名冲突。
/etc/nginx/options-ssl-proxy.conf

ssl_session_cache shared:proxy_nginx_SSL:1m;
ssl_session_timeout 1440m;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";

其中证书使用Let't Encrypt申请的免费证书,在此感谢Let't Encrypt的卓越服务。

接下来安装Tinyproxy,并启动服务:
pacman -Sy tinyproxy
systemctl enable tinyproxy
systemctl start tinyproxy
systemctl status tinyproxy
systemctl enable nginx
systemctl restart nginx
systemctl status nginx
配置文件在/etc/tinyproxy/tinyproxy.conf,使用默认配置就可以,默认监听127.0.0.1:8888。

测试:
curl --version,输出Features应该包含HTTPS-proxy,这样才支持HTTPS Proxy的测试。
curl https://httpbin.org/ip -x http://127.0.0.1:8888 -v,应该输出服务器IP,证明Tinyproxy 运行正常。
curl https://httpbin.org/ip -x https://your-domain.name:8443 -v,应该输出同样的内容,证明Nginx 运行正常。

如果测试正常,HTTPS Proxy基本上可用了,你可以到此为止。


但是Tinyproxy不支持用户认证,也就是说proxy authorization 不能使用。那么可以使用SSL的双向认证,在Nginx上设置ssl_verify_client on。在 server 段中加入以下配置:

ssl_client_certificate /etc/nginx/certs/client.crt;
ssl_verify_client on

客户端证书与网站证书不同,可以使用Easy-RSA 自己申请和管理,不需要第三方认证,client.crt 就是自己生成的Root CA。

客户端使用CA签发的证书与服务器连接,没有证书无法连接,Chrome 支持使用 AutoSelectCertificateForUrls 策略自动选证书。

 ./easyrsa init-pki
 ./easyrsa build-ca
 ./easyrsa gen-req proxy.example.name
 ./easyrsa sign-req client proxy.example.name

以上命令会询问CN,即你的域名。基本上Root CA即执行init-pki 时填写example.com,gen-req 时填写一个子域名,proxy.example.com。example.com 根据需要替换。

这时生成了pki 文件夹,里面要用的文件是pki/ca.crt,把内容写入/etc/nginx/certs/client.crt:
cat pki/ca.crt > /etc/nginx/certs/client.crt

接下来要把proxy.example.com 的证书转换为Chrome 可识别的p12 格式:
openssl pkcs12 -export -clcerts -in pki/issued/proxy.example.com.crt -inkey pki/privae/proxy.example.com.key -out proxy.example.com.p12

执行完会生成proxy.example.com.p12 文件,可以导入Chrome 使用,Linux 下进入 chrome://settings/certificates 执行导入,Windows 下双击打开,导入系统,Chrome 使用系统的证书管理程序。

客户端证书认证必须配合AutoSelectCertificateForUrls, Chrome 不会主动提示代理需要选择证书。
Linux 下创建/etc/opt/chrome/policies/managed/policies.json 文件,写入以下内容:

{
  "AutoSelectCertificateForUrls": ["{\"pattern\":\"https://proxy.example.com\",\"filter\":{\"ISSUER\":{\"CN\":\"example.com\"}}}"]
}

Windows 下导入以下注册表:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\AutoSelectCertificateForUrls]
"1"="{\"pattern\":\"https://proxy.example.com\",\"filter\":{\"ISSUER\":{\"CN\":\"example.com\"}}}"

chrome://policy/ 应该会出现对应的值。

如果以上步骤完成,那么使用SwitchyOmega 配置一个 HTTPS 代理, 服务器填写 proxy.example.com, 端口 8443,访问 https://httpbin.org/ip 应该能得到服务器IP。

以上就是所有步骤,增加客户端可以使用相同的证书进行导入,也可以使用Easy-RSA申请新的证书,只需执行gen-req和sign-req即可。不要忘记导入AutoSelectCertificateForUrls 的配置,查看chrome://policy/ 检查配置是否生效。


以上完成了HTTP1.1 over TLS 代理,这在大部分情况下工作良好,只是在高延迟的网络条件下有点慢。现在Chrome和Firefox对HTTP2代理也有内置支持,可以使用nghttpx将代理转换为HTTP2。
安装:pacman -Sy nghttp2
配置:/etc/nghttpx/nghttpx.conf

frontend=unix:/var/run/nghttpx-proxy.sock
backend=127.0.0.1,3128
private-key-file=/etc/letsencrypt/live/example.net/privkey.pem
certificate-file=/etc/letsencrypt/live/example.net/fullchain.pem
verify-client-cacert=/etc/nginx/certs/client.crt
verify-client=yes
http2-proxy=yes
frontend-http2-window-size=1048575
frontend-http2-connection-window-size=1073741823
workers=1

这里frontend=unix:/var/run/nghttpx-proxy.sock是为了使用nginx监听同一个8443端口支持http1.1和http2代理,并转发h2代理流量到nghttpx,nginx内部处理http1.1代理。一般来说没有必要,因为nghttpx同样支持http1.1代理。如果不使用nginx转发,可直接使用nghttpx作为代理服务,可以直接写成frontend=0.0.0.0,8443;tls


如果使用nginx 转发需要配置nghttpx-proxy.sock文件的权限为666。
systemctl edit nghttpx
填入:

[Service]
ExecStartPost=/usr/bin/chmod 666 /var/run/nghttpx-proxy.sock

/etc/nginx/nginx.conf的stream配置如下:

stream {
        upstream http_proxy {
                server 127.0.0.1:8888;
        }
        upstream nginx_proxy {
                server unix:/var/run/nginx-proxy.sock;
        }
        upstream nghttpx_proxy {
                server unix:/var/run/nghttpx-proxy.sock;
        }
        
        map $ssl_preread_alpn_protocols $proxy {
                ~/bh2/b          nghttpx_proxy;
                ~/bhttp/1.1/b    nginx_proxy;
        }
        
        server {
                listen 8443;
                ssl_preread on;
                
                proxy_pass $proxy;
        }

        server {
                listen unix:/var/run/nginx-proxy.sock ssl;
        
                ssl_certificate /etc/letsencrypt/live/example.net/fullchain.pem;
                ssl_certificate_key /etc/letsencrypt/live/example.net/privkey.pem;
                include /etc/nginx/options-ssl-proxy.conf;
                ssl_dhparam /etc/nginx/ssl-dhparams.pem;

                proxy_pass http_proxy;
        }
}

这样在8443端口就同时支持http1.1和http2代理了。Chrome浏览器在服务器支持h2的时候会自动选择h2代理。使用 --disable-http2启动参数可以禁用h2。因为在使用h2代理进行大流量下载的同时浏览网页会导致网页无法连接,这与http2的多路复用与流控有关,使用http1.1代理没有类似问题,因为每个连接都是独立的tcp连接。


如果不支持HTTP2代理的客户端可以在本地用nghttpx转换为http1.1代理,命令如下:

./nghttpx --http2-proxy -b'proxy.example.net,8443;/;proto=h2;tls' -f'127.0.0.1,8080;no-tls' --cacert=curl-ca-bundle.crt

本地就可以连接127.0.0.1:8080使用HTTP代理了。

参考文档:
使用 nghttpx 搭建 HTTP/2 代理
nghttpx-howto.html#http-2-proxy-mode
Easy-RSA - Github
Easy-RSA - ArchWiki
带你使用Nginx实现HTTPS双向验证
AutoSelectCertificateForUrls

原文链接:https://marskid.net/2018/06/14/how-to-deploy-https-proxy/