HTTP/1.1,定义于 1999 年,至今仍在流行。纵使人们试图在它上面添加各种黑科技,但它依然有各种各样的不足。终于,在 2015 年 5 月,HTTP/2 发布了。HTTP/2 基于 SPDY 而建,性能和特性较 HTTP/1.1 有了极大的提升,此外,虽然 HTTP/2 标准本身并没有强制 TLS 加密(HTTPS),但主流实现(Google Chrome, Mozilla Firefox)均要求 HTTP/2 被包裹在 TLS 中,因此,HTTP/2 + TLS(HTTPS)已是事实上的标准。
本文中,如无特殊说明,「HTTPS」指代「HTTP + TLS」,其中的 HTTP 可以是 HTTP/1.1, SPDY/3.1 或 HTTP/2;但由于几乎所有的 HTTP/2 实现全部要求 TLS,因此单说「HTTP/2」的话,一般指自带了 TLS 的 HTTP/2。
本文介绍使用 nghttpx 配合 Squid 搭建一个支持 HTTP/2 的 HTTPS 代理的方法。
nghttpx 本身并不是一个代理,它只是一个翻译器,因此如果我们需要一个支持 HTTP/2 的正向 HTTPS 代理,可以用一个 HTTP/1.1 的正向代理(如 Squid)和 nghttpx 接在一起实现。使用这样一个 HTTPS 代理,既可以享受 HTTP/2 对多连接的优化(提高客户端和代理服务器之间的连接流畅度),又可以享受外层 TLS 带来的加密和安全。且由于流量特征是 HTTPS,不仅额外开销小,而且在一些封锁严重的 ISP 里也能应用自如。(如封锁了 DTLS 流量的情况下,OpenConnect / AnyConnect 只能 TCP over TCP,效率很低)
一、需求
需求有两种,一种是客户端原生支持 HTTP/2 的,以下以 Chrome 为例:
+------------+ +------------+ +------------+ +------------+ | | | | | | | | | Chrome +----+ nghttpx +----+ Squid +----+ Internet | | | | | | | | | +------------+ +------------+ +------------+ +------------+
如图,nghttpx 与 Squid 部署于服务器上,客户端的 Chrome 与 nghttpx 用 HTTP/2 交流,nghttpx 将请求翻译成 HTTP/1.1 发给 Squid,最后 Squid 抓取了结果返回。
另一种是客户端不支持 TLS 的,以下以 Pidgin 为例:
+------------+ +------------+ +------------+ +------------+ +------------+ | | | | | | | | | | | Pidgin +----+ nghttpx +----+ nghttpx +----+ Squid +----+ Internet | | | | | | | | | | | +------------+ +------------+ +------------+ +------------+ +------------+
如图,Pidgin 将 HTTP/1.1 请求发给本机的 nghttpx,本机的 nghttpx 翻译成 HTTP/2 之后发给服务器上的 nghttpx。之后的过程和上一种相同。
二、工具
nghttp2 是一个很优秀的 HTTP/2 的 C 类实现。它的前身是 SPDY 库 spdylay,作者都是 Tatsuhiro Tsujikawa,同时他也是著名下载工具 Aria2 的作者。nghttp2 含有多个组件,其中的 nghttpx 程序,可以进行 HTTP/2 和 HTTP/1.1 之间的翻译,如果编译时链接了 spdylay,它也可以支持 SPDY/3.1。
如果你是 Arch Linux 用户,可以直接使用我维护的 aur/nghttp2
包,直接 yaourt -S nghttp2
即可,吃豆人会帮你照料好剩下的一切。
如果你是 Debian / Ubuntu 用户,请按照官方 README 完成编译操作,编译完成后在 contrib
目录里可以找到 Upstart 配置文件。注意:nghttp2 库默认是不带 SPDY/3.1 支持的,如果需要 SPDY 支持,请先编译 spdylay 再编译 nghttp2,后者会自动检测到 spdylay 的存在并链接。
如果你是 CentOS 用户,祝您今天有个好心情。
三、服务器配置
无论是哪种需求,服务器上都需要 nghttpx 和 Squid。
nghttpx
服务器上 nghttpx,前端接受的是来自客户端的 HTTP/2 请求,后端是 Squid,最小配置是这样:
frontend=0.0.0.0,443;tls #backend=127.0.0.1,3128;no-tls backend=127.0.0.1,3128;;no-tls private-key-file=/path/to/private/key certificate-file=/path/to/certificate http2-proxy=yes
其中私钥和证书必须是客户端认可的。你可以选择:
- 去 NameCheap 之类的网站上买一个商业证书,低至 $9 一年;
- 自己用 OpenSSL / GnuTLS 等工具签一个,然后在你的客户端里强制设置为信任;
- 如果你不愿意花钱也不愿意折腾 OpenSSL,那你可以尝试去找家免费的 CA 给你签一个。
需要说明的是,GFW 曾被报道会区分商业证书和野证书并对后者做定点清除。试图使用野证书的同学请将此因素考虑在内。
以上只是最小配置,我个人使用的配置还加上了以下内容,是我在 nghttpx 的文档中挑出来觉得比较有用的选项:
# 使用四个 worker,请根据自己服务器的 CPU 合理调整,太小性能差,太大机器挂 workers=4 # 开启客户端 TLS 认证 verify-client=yes verify-client-cacert=/path/to/client/ca # 不添加 X-Forwarded-For 头 add-x-forwarded-for=no # 不添加 Via 头 no-via=yes # 不查询 OCSP 服务器 no-ocsp=yes # 指定 NPN / ALPN 的顺序 #npn-list=spdy/3.1,h2 →这一行已经不用加了,见下 # 只使用 TLS 1.2 tls-proto-list=TLSv1.2 # 只使用 ECDHE 交换(目前性能安全比最优)和 AES 加密,指定 128 位是因为它已经足够安全但性能比 256 位稍优一些 ciphers=ECDHE+AES128 # 开启日志功能 accesslog-file=/var/log/nghttpx/access.log accesslog-format=$remote_addr [$time_iso8601] "$request" $status $body_bytes_sent $alpn "$http_user_agent"
有关 --npn-list
选项:前文已经说明了,nghttp2 如其名字所示,是一个 HTTP/2 的库,但是由于 Chromium / Google Chrome 的一个 bug(发稿时最新的 v45 仍未修复已在 v46 中修复),Cr 对 HTTP/2 代理的支持有点问题(Firefox nightly 没有问题),而 nghttpx 默认的 NPN / ALPN 顺序是 h2 优先的,所以需要在这里把 spdy/3.1 的优先级调成最高,以便让 Cr 能用 SPDY/3.1 协商……如果它在编译时链接了 spdylay 的话,也能向下支持(已过时的)SPDY。这一选项是调节优先使用哪种协议的。Chrome 46 之后对 HTTP/2 代理的支持已经正常了,这个选项可以不用调了。
有关 --verify-client
功能:请看下文「有关鉴权」一节。
Squid
Squid 是一个久经考验的正向代理。在我们的用例中,它是 nghttpx 的后端,只需监听 localhost 即可。我用的最小配置如下:
http_port 127.0.0.1:3128 http_access allow localhost # 关闭缓存功能和日志功能 cache deny all access_log none # 优先访问 IPv4 站点,有完整 IPv6 支持的机器可以去掉 dns_v4_first on # 不添加 Via 头 via off # 删除 X-Forwarded-For 头 forwarded_for delete
我曾试图让 nghttpx 把源 IP 地址发给 Squid 然后让 Squid 记到日志里,但是未能成功,于是 Squid 始终只能记到一堆来自 127.0.0.1 的请求,干脆就把 Squid 的日志关闭,让 nghttpx 去记日志了。其实是可以在 nghttpx 配置里保留 X-Forwarded-For 然后在 Squid 配置里加上 follow_x_forwarded_for allow localhost
让 Squid 能记录到原始的来源 IP 地址。感谢 @JmyXu 的指正。
一个可能会让强迫症不爽的地方是,Squid 默认的错误页面会引用 Squid 官网的图片(一只乌贼),而这个图片资源是 http:// 的,因此页面会带有「混合内容」,强迫症用户可以通过编辑 errorpage.css 把这个去掉:
background: url('http://www.squid-cache.org/Artwork/SN.png') no-repeat left;
或者像我一样把这个图片换成 data:image/png;base64
嵌在 CSS 里……
有关鉴权
只按照最小配置来做的话,配置出来的 HTTP/2 代理是没有任何鉴权的,任何人都可以把这个地址填进 Chrome 里当代理用,也就是说,这是一个开放代理。但实践证明:
- 如果你在公网上搭一个不带 TLS 的 HTTP/1.1 开放代理的话,分分钟各种爬虫就会把你的地址撸走,教你做人;
- 如果你在公网上搭一个带 TLS 的 HTTP/1.1 开放代理的话,来光顾你的爬虫就非常非常少了,几个月也遇不到几只;
- 如果你在公网上搭一个只允许 TLS 1.2 的 HTTP/2 开放代理的话,根本不会有爬虫来光顾你……
所以,如果不想弄鉴权的话,问题也不大,因为目前根本没有 TLS 1.2 + HTTP/2 的爬虫,除非你主动把地址告诉别人,否则不会有人来用你的代理。不过,这样毕竟只是迷宫,而不是门锁,所以为了安全还是可以配置一下鉴权。
在这种 TLS 1.2 + HTTP/2 的结构下,鉴权可以在两个阶段做:TLS 和 HTTP(感觉是废话),也就是 nghttpx 和 Squid(好像还是废话)。
在 TLS 层面做鉴权的话,就是用上文所述的 --verify-client
了。你需要自己维护一个 CA,然后把 CA 的根证书放到服务器上,持有该 CA 根证书的私钥签出的证书对应的私钥的用户可以使用该代理,否则根本完成不了 TLS 握手,直接被拒绝。CA 的搭建和管理又是一个巨大的话题了,在此不多做叙述,只是推荐一下两个软件:适用于 GNU/Linux 用户的 XCA,和适用于 OS X 用户的 Keychain。这两个都是能够管理中小型 CA 的 GUI 程序。我个人使用的则是 EasyRSA。当然如果你足够硬核,也可以直接使用命令行的 OpenSSL 去管理 CA。再次强调,这个 CA 只是客户端认证所用的,和你买证书的那种商业 CA 没有也不应该有联系。
在 HTTP 层面做鉴权的话,请照着 Squid 官方文档做。
推荐用 TLS 鉴权,你会爱上它的。而且 TLS 鉴权的话,Chrome 能用 AutoSelectCertificateForUrls 策略自动选证书,不用每次开 Chrome 的时候点一下。
四、客户端配置
客户端配置分为两种。Chrome 和 Firefox 等直接支持 HTTP/2 代理的,直接填进去就行。大部分不支持 TLS 的程序,需要在本地再起一个 nghttpx,翻译一下,在本地生成一个 HTTP/1.1 的代理,供程序使用。
无需翻译的程序
Chromium / Google Chrome 理论上支持 HTTPS 代理的,但是如上文所述,目前由于一个 bug 的存在,对 HTTP/2 代理的支持有问题,暂时只能用 SPDY/3.1;Firefox 曾经不能正常使用 HTTPS 代理(当年 Chrome 是唯一能使用 HTTPS 代理的浏览器),现在它的 nightly 版本反而是支持 HTTP/2 了而超越了 Cr……(bug 已经修复)
令人郁闷的是,无论是 Cr 还是 Fx,都不能方便地通过 GUI 配置 HTTPS 代理,只能通过命令行或插件的方式来使用 HTTPS 代理。比如这样一个 pac 文件便可以让 Cr 和 Fx 使用 HTTPS 代理了:
function FindProxyForURL(url, host) { return "HTTPS proxy.example.org:443"; }
当然,pac 文件可以写得非常复杂,也可以使用浏览器插件进行更灵活的代理配置。Cr 用户推荐使用 SwitchyOmega。
需要翻译的程序
如前文所述,目前除了 Cr 和 Fx,大部分软件是不支持 HTTP/2 的,而 nghttpx 是个 HTTP/1.1 和 HTTP/2 的翻译器,因此我们可以在本机起一个 nghttpx 生成一个 HTTP/1.1 的代理供不支持 HTTP/2 的程序使用。这种情况下,nghttpx 的前端接收 HTTP/1.1 的请求,然后翻译成 HTTP/2 发给服务器上的另一个 nghttpx 实例。最小配置如下:
frontend=127.0.0.1,8080;no-tls backend=proxy.example.org,443;proto=h2;tls http2-proxy=yes
同样地,这只是最小配置,我个人使用的配置中还有以下选项:
# 认证用证书和私钥,如果你没用 TLS 认证则不需要 client-cert-file=/path/to/certificate client-private-key-file=/path/to/private/key # 四个 worker,请根据自己计算机/手机性能调整 workers=4 # 不添加 X-Forwarded-For 头 add-x-forwarded-for=no # 不添加 Via 头 no-via=yes # 不查询 OCSP no-ocsp=yes # NPN / ALPN 优先使用 h2 npn-list=h2
另外可能有用的选项是 -k
。如果你用了野证书,这个选项让 nghttpx 放弃证书校验(不安全!),此外它在手机等慢速网络下也有缩短启次握手时间的效果。另外如果你想看实时请求情况的话,加上 -L INFO
能看到漂亮的彩色输出。这个 nghttpx 跑起来之后,别的程序设置 http://127.0.0.1:8080
为代理即可使用。
刚才提到了手机。是的,手机。Tatsuhiro Tsujikawa 大大的程序都是为 Android 交叉编译优化过的。(什么,您是 iOS 用户?您还是用您的 APN 代理,也就是 HTTP 明文代理吧……)
在 nghttp2 的文档中,提供了两种方便的交叉编译 Android 版 nghttpx 的方法,一种是自己装 Android NDK 和依赖,然后用 android-config
和 android-make
脚本自动做。另一种是用 Dockerfile.android
文件,在 Docker 容器里装上乱七八糟的编译环境和依赖,最后产出珍贵的 nghttpx
文件并复制到容器外面来。编译完的二进制文件记得 strip 一下,能从 11 MiB 减到 2 MiB……
在 Android 上运行起 nghttpx 之后(可以用 JuiceSSH 之类的起一个,无需 root),推荐配合 Drony 使用(也无需 root),该应用使用 VpnService()
捕获所有应用流量,然后再按照你定的规则(来源地址、应用名、目标主机名、目标端口号、HTTP 方法等),将这些流量进行分流(直连、截断、传给代理、交给 pac 处理等)。
五、尾声
我竟然已经整整一年没有写博客了。一年里可以写的东西其实不少,但是由于各种原因的确没怎么写。今天因某人提醒我一年没更新了,又正好手头的事情告一段落,于是便这么写了一篇。也算是能造福一些人吧。
最后,Google Chrome 使用 HTTP/2 代理看 YouTube 4k 效果如下:
本文原载于: https://wzyboy.im/post/1052.html 。如有转载请注明。
读者来信
2021-05-18 读者丰 <h...@gmail.com> 来信指出,文中 nghttpx 的最小配置 backend=
这一行少了一个英文分号并误导了一些读者(1、2)。这一配置错误是因为 nghttpx 版本升级导致配置文件语法不兼容所致。我已在原文中修复这一问题。