---
id: 6e0bce9e22a4bf7d9672c59e67bcf5ec
author: Lemon Mint
title: gosuda.org/chromenet：Chromium 面向 Go 的网络栈，基于固定版本发布构建且可复现
description: CGO_ENABLED=0 Go http.RoundTripper，通过 Chromium 自身的 //net 网络栈（HTTP/1.1、HTTP/2、QUIC/HTTP/3、BoringSSL）发送请求，运行于隔离的 HQQ 工作进程中，其源代码、工具链以及六个嵌入式二进制文件均经 SHA-256 固定校验，并在 gvx 沙箱中构建。
language: zh
date: 2026-11-09T00:00:00Z
path: /chromenet
go_package: gosuda.org/chromenet
go_repourl: https://github.com/gosuda/chromenet.git
category: network
---

`gosuda.org/chromenet` 是一个 Go 语言的 `http.RoundTripper`，它通过 Chromium 自身的网络栈发送请求。该网络栈来自官方 Chromium 发行版中的 `//net`：包括其 HTTP/1.1、HTTP/2 和 QUIC/HTTP/3 实现、BoringSSL、Chrome 的 TLS 配置以及 Chrome Root Store。它在独立的工作进程 `chrome_net_server` 中运行，Go 一侧保持纯 Go（`CGO_ENABLED=0`），并通过 [HQQ](https://gosuda.org/hqq) 共享内存 RPC 与工作进程通信。

```bash
go get gosuda.org/chromenet@v0.1.10157081033800
```

## 为何运行 Chromium 的网络栈

使用 `net/http` 与网络交互的 Go 程序，会向服务器呈现为 Go 客户端。对于服务间通信，这是正确的默认选择。但当服务器的行为取决于其认为的客户端类型时，这种做法就不再合适：例如仅限浏览器访问的端点、反机器人中间件、针对不同 TLS 和 HTTP/2 指纹提供不同协议版本的 CDN，以及必须复现真实浏览器体验的测试。

Go 标准库无法提供浏览器的线路层行为。`crypto/tls` 有其自身的密码套件顺序和扩展集合，`net/http` 也有其自身的 HTTP/2 设置。通过复制浏览器 ClientHello 字节来模仿浏览器的库，必须在每个版本中持续复制。`chromenet` 采取了另一条路径：它不模仿 Chromium 网络栈，而是直接运行它。

在 `darwin/arm64` 引擎上，针对公共指纹服务（`tls.peet.ws`）进行测量，默认配置报告如下：

| 属性                               | 观测值                                                                |
| :--------------------------------- | :-------------------------------------------------------------------- |
| 协商协议                           | `h2`                                                                  |
| JA4                                | `t13d1516h2_8daaf6152771_806a8c22fdea`                                |
| HTTP/2 指纹（Akamai 格式）         | `1:65536;2:0;4:6291456;6:262144\|15663105\|0\|m,a,s,p`                |
| User-Agent                         | 与引擎主版本一致的桌面 Chrome，例如 `Chrome/157.0.0.0`                |

启用 `ForceHTTP3` 后，对 `cloudflare-quic.com` 和 `www.google.com` 的请求通过 HTTP/3 得到响应。这些数值来自某一天的某一平台。引擎即是该网络栈本身，因此当锁定的 Chromium 版本变更时，这些数值也会随之变化。

## 使用方法

`NewTransport` 返回一个 `http.RoundTripper`。凡是可以使用 `http.Transport` 的地方均可使用它：

```go
package main

import (
	"fmt"
	"io"
	"net/http"

	"gosuda.org/chromenet"
)

func main() {
	client := &http.Client{Transport: chromenet.NewTransport(chromenet.Options{})}

	resp, err := client.Get("https://example.com/")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Proto, resp.StatusCode, len(body))
}
```

首次请求时，模块会将适用于当前平台的嵌入式工作进程二进制文件写入用户缓存目录，使用 `gosuda.org/hqq/secureexec` 验证其 SHA-256 摘要，以父进程生命线启动它（`--parent-stdin`，使其随您的进程退出而退出），并打开一个会话池。无需安装任何内容。

为使 `http.Client` 无需修改即可工作，部分行为与 `http.Transport` 保持一致：

- Transport 不跟随重定向，也不保存 Cookie。与 `http.Transport` 一样，`http.Client` 会执行这两项操作。
- 使用 gzip、deflate、Brotli 或 zstd 压缩的响应会被解码后返回，且 `resp.Uncompressed` 为 true。
- 在响应正文关闭之前，一个请求会占用一个引擎会话。请务必关闭正文。
- 取消请求上下文会在 Chromium 内部取消该请求。
- 网络故障以 `*client.NetError` 值的形式返回，其中包含 Chromium 错误码和名称：

```go
var ne *client.NetError
if errors.As(err, &ne) {
	fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
}
```

### 选项

| 字段                          | 作用                                                                                                       |
| :---------------------------- | :--------------------------------------------------------------------------------------------------------- |
| `UserAgent`                   | 替换默认的 Chrome User-Agent。请求头中的 `User-Agent` 仍然优先生效。                                        |
| `AcceptLanguage`              | 替换 `en-US,en;q=0.9`。                                                                                    |
| `Proxy`                       | Chromium 代理规则，例如 `http://127.0.0.1:8080` 或 `socks5://127.0.0.1:1080`。为空则直连。                 |
| `DisableHTTP2`, `DisableQUIC` | 关闭相应协议。                                                                                             |
| `ForceHTTP3`                  | 对所有源使用 QUIC，无需等待 `Alt-Svc` 通告。                                                               |
| `InsecureSkipVerify`          | 接受任何服务器证书。                                                                                       |
| `Pool`                        | 要使用的引擎会话。为 nil 时使用 `client.DefaultPool()`，其允许 64 个并发请求。                             |

选项相同的 Transport 共享同一个 Chromium `URLRequestContext`，因此连接池、HTTP/2 会话和 QUIC 会话可在请求之间复用。设置 `HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server` 可替换嵌入的工作进程，例如使用本地构建版本。

## 工作原理

| 层级                | 运行内容                                                                                                                                                                 |
| :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 您的程序            | `http.Client` 与 `chromenet.Transport`，纯 Go 实现。                                                                                                                     |
| 进程边界            | HQQ 共享内存 RPC。接口为 `chrome_net.idl`，由 `hqq-bindgen` 编译为 Go 客户端和 C 服务端胶水代码。                                                                         |
| `chrome_net_server` | 构建于 Chromium 之上的小型 C++ 层：一个拥有全部 `//net` 对象的网络线程，以及在请求上阻塞直至网络线程应答的会话线程。                                                       |
| Chromium `//net`    | `URLRequestContext`、`URLRequest`、HTTP/2 和 QUIC 会话、BoringSSL。                                                                                                      |

IDL 包含六个关键操作：从 `key=value` 选项创建引擎；以请求头（`METHOD URL` 加上头部行）和正文长度开始请求；写入请求正文分块；等待响应头；读取解码后的响应正文；以及销毁句柄。请求句柄声明了 `@cancel`，因此被放弃的调用会在 Chromium 内部中断请求，而不是任其运行至完成。

将网络栈隔离在工作进程中，既是一种打包方式，也是一项安全属性。Chromium 的网络代码需要解析来自开放互联网的恶意输入。若它崩溃，您的 Go 服务不会随之崩溃：调用会返回错误，会话被丢弃，客户端在下次需要时启动新的工作进程。

## 可复现构建，通过 SHA-256 锁定

包含 Chromium 的 25 MB 二进制文件，只有在您能够准确说明其构成内容时才值得信赖。`chromenet` 跟踪 Chromium Stable 的方式，与 `gosuda.org/sqlite3` 跟踪 SQLite 的方式相同：源代码从不纳入仓库，只进行锁定。

| 锁定项                                                                                                                   | 位置                           |
| :----------------------------------------------------------------------------------------------------------------------- | :----------------------------- |
| Chromium 发行版及其官方 `-lite` 源码压缩包                                                                               | `third_party/chromium/VERSION` |
| 压缩包以及该发行版锁定的每个工具链的 SHA-256：clang、Rust、GN、ninja、Windows clang 运行时、xwin                         | `third_party/chromium/SHA256`  |
| 通过 xwin 获取的 MSVC CRT 与 Windows SDK 版本                                                                            | `third_party/chromium/WINSDK`  |
| 沙箱运行时（`rungv`）发行版、客户机镜像摘要、Debian 快照日期                                                            | `scripts/engine/BUILDENV`      |
| HQQ C 运行时                                                                                                             | `go.mod` 与 `go.sum`           |

当前锁定的版本为 Chromium `157.0.8103.38`。若下载内容与锁定值不符，构建将终止。该发行版对 Chromium 仅应用一个补丁：在 Linux 上，`chrome_net_server` 构建时不使用 NSS，并且仅依据 Chrome Root Store 验证服务器证书，因此该二进制文件除 libc 外无需任何其他依赖。

Linux 和 Windows 引擎构建于 [gvx](https://gosuda.org/blog/posts/gvx-cross-platform-linux-containers-z48916d01) 沙箱中：这些是无特权进程，使用锁定的客户机镜像，且没有宿主守护进程。Windows 引擎在 Linux 上使用 Chromium 的 clang-cl 和 lld-link 进行交叉编译，并针对 xwin 从 Microsoft 获取的 Windows SDK 进行链接。macOS 引擎则在 macOS 运行器上原生构建，因为 Chromium 的 macOS 构建需要 Xcode。

对同一提交进行重新构建，在不同的 CI 运行中生成了逐字节相同的二进制文件。在全部六个平台上，发布的二进制文件与早前 CI 运行为将引擎引入 monorepo 的提交所构建的二进制文件逐字节相同。

| 平台            | 二进制大小 |
| :-------------- | ---------: |
| `linux/amd64`   |    27.9 MB |
| `linux/arm64`   |    27.4 MB |
| `darwin/amd64`  |    26.2 MB |
| `darwin/arm64`  |    23.9 MB |
| `windows/amd64` |    27.3 MB |
| `windows/arm64` |    25.8 MB |

Linux 引擎仅依赖 `libc`、`libm`、`libdl`、`libpthread` 和 `libgcc_s`，并需要 glibc 2.31 或更高版本。

### 跟踪 Chromium 与发布

一个定时工作流每六小时检查一次 Chromium Stable。若出现新版本，它会重写锁定信息，在 `chromium/<version>` 分支上构建并测试全部六个引擎，为二进制文件生成证明，并将锁定信息连同嵌入的二进制文件、其摘要以及许可证文本，在一次提交中推送到 `main`。在 monorepo 中，各文件的 `OWNERS` 仅授予该工作流的机器人对这些文件的权限，因此更新无需人工介入即可合入；而对构建方案本身的更改（Windows SDK 锁定和补丁）仍需负责人审批。

每个发布的二进制文件都带有构建该文件的工作流所生成的构建来源证明：

```bash
gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet
```

每个二进制文件所包含内容（Chromium、BoringSSL、QUICHE 以及构建图中的其他组件）的许可证文本，均由 Chromium 自带的 `tools/licenses/licenses.py` 生成，并随发布版本以 `NOTICE_<os>_<arch>.txt` 的形式提供。

## 限制

- `DialWebSocket` 是一个纯 Go 实现的 RFC 6455 客户端，不使用 Chromium 网络栈。
- `DialWebTransport` 是进程内实现，无法连接服务器。
- 默认的 JA4 和 HTTP/2 指纹取决于所锁定的 Chromium 版本。Chromium 会在各版本之间更改这些指纹，而 `chromenet` 会随之跟进。
- macOS 二进制文件采用临时签名（ad-hoc），未经过公证。
- 工作进程会占用内存。每个引擎二进制文件在磁盘上占 24 至 28 MB，且 Chromium 运行时会启动线程池和网络线程。
- 引擎会话在响应正文关闭之前一直被占用。若程序泄漏响应正文，将耗尽连接池。

## 源代码

代码仓库为 [github.com/gosuda/chromenet](https://github.com/gosuda/chromenet)，镜像自 gosuda monorepo 中的 `//net/chromenet`。构建方案位于 `scripts/engine`，引擎胶水代码位于 `engine/`，接口定义位于 `chrome_net.idl`。
