---
id: 6e0bce9e22a4bf7d9672c59e67bcf5ec
author: Lemon Mint
title: 'gosuda.org/chromenet: 固定されたリリースから再現可能にビルドされた、Go向けのChromiumネットワークスタック'
description: CGO_ENABLED=0 の Go http.RoundTripper であり、分離された HQQ ワーカー内で Chromium 独自の //net スタック(HTTP/1.1、HTTP/2、QUIC/HTTP/3、BoringSSL)を通じてリクエストを送信します。ソース、ツールチェーン、および6つの埋め込みバイナリは SHA-256 によって固定され、gvx サンドボックス内でビルドされています。
language: ja
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` は、Chromium 自身のネットワークスタックを通じてリクエストを送信する Go の `http.RoundTripper` である。このスタックは公式の 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` を用いて Web と通信する 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))
}
```

最初のリクエストの際、このモジュールは埋め込まれたワーカーバイナリをお使いのプラットフォーム向けに選択し、ユーザーキャッシュディレクトリに書き出す。その SHA-256 ダイジェストを `gosuda.org/hqq/secureexec` で検証し、親プロセスとのライフライン（`--parent-stdin`、これによりお使いのプロセスが終了するとワーカーも終了する）を付けて起動した後、セッションプールを開く。いかなるものもインストールする必要はない。

`http.Client` が変更なしで動作するよう、いくつかの振る舞いは `http.Transport` に準拠している。

- Transport はリダイレクトに従わず、Cookie も保持しない。`http.Client` は、`http.Transport` の場合と同様にこれらを行う。
- gzip、deflate、Brotli、zstd で圧縮されたレスポンスは復号された状態で届き、`resp.Uncompressed` は true となる。
- リクエストは、そのレスポンスボディが閉じられるまで一つのエンジンセッションを保持する。ボディは必ず閉じること。
- リクエストのコンテキストをキャンセルすると、Chromium 内部のリクエストもキャンセルされる。
- ネットワークの失敗は、Chromium のエラーコードおよび名称を保持する `*client.NetError` の値として返される。

```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`                       | `http://127.0.0.1:8080` や `socks5://127.0.0.1:1080` などの Chromium のプロキシルール。空の場合は直接接続となる。 |
| `DisableHTTP2`, `DisableQUIC` | 各プロトコルを無効にする。                                                                             |
| `ForceHTTP3`                  | `Alt-Svc` の通知を待たず、すべてのオリジンに対して QUIC を使用する。                                    |
| `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` は、`gosuda.org/sqlite3` が SQLite に従うのと同様に Chromium Stable に従う。ソースはベンダリングされることはなく、固定（ピン留め）されるのみである。

| 固定対象                                                                                                                 | 場所                           |
| :----------------------------------------------------------------------------------------------------------------------- | :----------------------------- |
| Chromium のリリースおよびその公式 `-lite` ソースアーカイブ                                                                | `third_party/chromium/VERSION` |
| アーカイブ、およびそのリリースが固定するすべてのツールチェーン（clang、Rust、GN、ninja、Windows 向け clang ランタイム、xwin）の SHA-256 | `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 のエンジンは、Chromium の clang-cl と lld-link を用いて Linux 上でクロスコンパイルされ、xwin が Microsoft から取得する Windows SDK に対してリンクされる。macOS のエンジンは macOS ランナー上でネイティブにビルドされる。これは、Chromium の macOS ビルドに Xcode が必要となるためである。

同一のコミットを再ビルドした場合、別々の CI 実行においてもバイト単位で同一のバイナリが生成された。リリースバイナリは、エンジンをモノレポに取り込んだコミットに対して以前の CI 実行がビルドしたものと、六つすべてのプラットフォームにおいてバイト単位で同一である。

| プラットフォーム | バイナリサイズ |
| :--------------- | -------------: |
| `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 とリリースの追跡

スケジュールされたワークフローが six 時間ごとに Chromium Stable を確認する。新しいリリースが見つかると、固定値を書き換え、`chromium/<version>` ブランチ上で六つのエンジンすべてをビルドおよびテストし、バイナリに対する証明を付与し、固定値を埋め込まれたバイナリ、そのダイジェスト、およびライセンス文書とともに、一つのコミットとして `main` にコミットする。モノレポにおいては、ファイルごとの `OWNERS` によって、ワークフローのボットにこれらのファイルのみが付与されるため、人の手を介さずに更新が反映される。ただし、ビルドレシピそのものの変更（Windows SDK の固定値やパッチ）については、引き続きオーナーの対応が必要である。

リリースされたすべてのバイナリには、それをビルドしたワークフローによるビルド来歴の証明（attestation）が付与される。

```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 のバイナリはアドホック署名されており、公証（notarization）は行われていない。
- ワーカープロセスはメモリを消費する。エンジンのバイナリはディスク上で 24〜28 MB あり、Chromium のランタイムはスレッドプールとネットワークスレッドを起動する。
- エンジンセッションは、レスポンスボディが閉じられるまで保持される。レスポンスボディをリークするプログラムは、プールを枯渇させる。

## ソース

リポジトリは [github.com/gosuda/chromenet](https://github.com/gosuda/chromenet) であり、gosuda モノレポの `//net/chromenet` からミラーされている。ビルドレシピは `scripts/engine` に、エンジンのグルーコードは `engine/` に、インターフェースは `chrome_net.idl` にある。
