GoSuda

教你的 Go 程序像 Chrome 一样说话

By Lemon Mint views ...

如果你曾经出国旅行并用教科书式的标准短语点过一杯咖啡,你一定见过咖啡师投来的那种眼神。你的语法没有问题,每个单词都能在词典里找到。然而不到半秒钟,咖啡馆里的每个人就都知道你是外地人。

Go 的标准 HTTP 客户端也有类似的问题。它是一个出色的默认选择:可靠、简洁,开箱即可用于严肃的工作。然而,当你的程序连接服务器时,它显然仍像一个 Go 程序。大多数情况下,没有人会在意。不过,一些网站、CDN 和测试环境会密切关注是谁在敲门,并且会根据它们认为所面对的客户端类型而表现出截然不同的行为。

这正是我们构建 gosuda.org/chromenet 的原因。与其让 Go 的标准客户端勉强模仿浏览器,chromenet 允许 Go 程序使用 Google Chrome 所采用的同一网络引擎与网络通信,即 Chromium 自身的网络代码。无论你是想检验自己的测试环境如何对待浏览器流量,还是要与位于挑剔 CDN 之后的网站通信,通过 Chrome 的引擎进行交互都能消除一整层的不确定性。

你无需重写应用程序即可使用它。在你的代码中,它可以作为 http.Transport 的直接替代品使用。你依然可以像以往一样使用 http.Client,而底层的通信则由 chromenet 负责处理。

要试用最新版本,请使用 go get 进行安装:

1go get gosuda.org/chromenet@v0.2.10157081033800

然后将其传输层交给你的客户端:

1client := http.Client{
2	Transport: chromenet.NewTransport(chromenet.Options{}),
3}

在此版本中,现代网络流量的全部范围都经由 Chrome 的引擎传输。它现在涵盖普通网页请求、WebSocket 和 WebTransport。早期版本仅能正确处理普通请求,而 WebSocket 和 WebTransport 只是替代实现。这意味着你在门口的初次问候听起来像 Chrome,但一旦切换到另一种交互方式,你的伪装就可能露馅。此版本用真正的实现取代了这些替代品。

等一下,如果我们使用的是 Chromium 自身的网络代码,那是否意味着你原本简洁的 Go 构建会变成一场庞大的 C 编译器噩梦?所幸并非如此。你的 Go 代码依然是纯粹的 Go,无需 cgo,也不需要 C 编译器。

chromenet 并不会把浏览器代码直接焊接进你的二进制文件,而是将引擎作为一个小型独立程序,与你的 Go 程序并行运行。可以把它想象成聘请一位流利的本地口译员,他与你并肩同行,而不是试图重新构造你自己的声带。由于引擎运行在独立的程序中,其内部的崩溃不会导致你的服务中断。

chromenet 的工作原理:你的 Go 程序向一个小型配套程序发出请求,该程序运行 Chromium 的网络引擎并与网络通信

你的 Go 程序保持为纯 Go,而一个小型配套程序则通过 Chromium 的网络引擎承载流量。

在你的工作站或服务器上,也无需额外安装任何东西。适用于 Linux、macOS 和 Windows 的引擎二进制文件,同时支持 Intel/AMD 与 ARM 架构,均直接打包在 Go 模块中,并在运行前经过校验。

在模块中附带二进制文件,自然会引发任何关注信任问题的人的疑问,因此我们希望给出一个直接的答案。这些二进制文件以可复现的方式,由特定的 Chromium 版本构建,目前为 Chromium 157,因此任何人都可以重新构建并获得完全相同的字节,而且每个版本都经过 GitHub 的证明(attestation)。为了无需手动操作即可跟上浏览器的发展,一个定时任务会追踪 Chromium 的稳定通道,并在新版 Chrome 发布时自动更新版本。

那么 uTLS 呢?

如果你以前尝试过让 Go 客户端看起来像一个浏览器,你很可能已经接触过 uTLS。它是一项出色的成果:这是一个 Go 库,允许你装扮安全连接的第一条消息,使其类似 Chrome、Firefox 或任何你想要的客户端。它体积小、纯 Go 实现,并且除了你的程序之外不会额外运行任何东西。

关键在于这套伪装的覆盖范围有限。uTLS 只涵盖握手,即对话开头的几句话。其后的一切仍需你自行构建:HTTP/2 层必须手动接入并调优,HTTP/3 则是另一项独立的工程,而 WebSocket 或 WebTransport 则意味着更多的胶水代码。Chrome 预设也只是副本,因此每当 Chrome 改变其行为习惯时,都需要有人持续更新它们。这有点像模仿一个人的口音,而他的其余肢体语言依然会暴露他的身份。

chromenet 采取了相反的路线。它不模仿 Chrome,而是请 Chrome 自身的引擎来完成对话,因此握手、连接复用方式、协议升级以及传输层都来自同一个源头,并会随着 Chromium 的演进而同步变化。

这种取舍是坦诚的。uTLS 更轻量,允许你设计任何你能想象到的问候方式,而 chromenet 则需要一个 25 MB 的辅助程序,但它提供的是 Chrome 的真实行为,而非一种定制化的行为。如果你希望依赖极小、并对握手拥有完全控制权,uTLS 仍然是一个很好的选择。

每种设计都有其权衡,我们希望在此坦率地说明其局限。封装真正的浏览器网络引擎并不小巧:每个引擎二进制文件约为 25 MB。在 macOS 上,这些二进制文件未经过公证(notarized)。而在 Linux 上,你的系统需要 glibc 2.31 或更高版本。如果这些限制符合你的环境,那么只需替换一个传输字段,你便能获得真正的 Chrome 网络能力。

该项目位于 github.com/gosuda/chromenet。如果你了解 gosuda 开源家族中的姊妹项目 gosuda.org/hqq 和 gosuda.org/sqlite3,你会发现其理念十分熟悉:从更广阔的软件世界中引入功能强大但体量庞大的技术,并使其在纯 Go 中自然融入。

因此,如果你因为需要让 Go 程序听起来像一个浏览器而选择 uTLS,不妨试试 chromenet。它可以作为 uTLS 的良好替代方案,尤其是当你需要的不仅仅是 HTTPS 请求时:HTTP/2、HTTP/3、WebSocket 和 WebTransport 都通过同一个 Chrome 引擎传输,你无需编写任何胶水代码。