---
id: 6e0bce9e22a4bf7d9672c59e67bcf5ec
author: Lemon Mint
title: 'gosuda.org/chromenet: сетевой стек Chromium для Go, собранный воспроизводимым способом из зафиксированного релиза'
description: CGO_ENABLED=0 Go http.RoundTripper, который отправляет запросы через собственный стек //net Chromium (HTTP/1.1, HTTP/2, QUIC/HTTP/3, BoringSSL) в изолированном воркере HQQ, с исходным кодом, toolchain и шестью встроенными бинарными файлами, зафиксированными по SHA-256 и собранными в песочницах gvx.
language: ru
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. Этот стек представляет собой `//net` из официального релиза Chromium: реализации HTTP/1.1, HTTP/2 и QUIC/HTTP/3, BoringSSL, конфигурация TLS Chrome и Chrome Root Store. Он работает в отдельном рабочем процессе `chrome_net_server`, а сторона Go остаётся чистым Go (`CGO_ENABLED=0`) и обменивается данными с рабочим процессом через RPC на основе разделяемой памяти [HQQ](https://gosuda.org/hqq).

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

## Зачем запускать сетевой стек Chromium

Go-программа, которая обращается к сети через `net/http`, представляется серверу как Go-клиент. Это правильный выбор по умолчанию для трафика между сервисами. Однако он неподходящ, когда поведение сервера зависит от того, с каким типом клиента он себя ассоциирует: эндпоинты, доступные только браузерам, промежуточное ПО для защиты от ботов, CDN, которые отдают разные версии протоколов клиентам с разными отпечатками TLS и HTTP/2, а также тесты, которые должны воспроизводить то, что испытывает реальный браузер.

Стандартная библиотека 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.Transport`, благодаря чему `http.Client` работает без изменений:

- Transport не следует перенаправлениям и не хранит cookie. `http.Client` делает и то и другое, как и в случае с `http.Transport`.
- Ответы, сжатые 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`                   | Заменяет User-Agent Chrome по умолчанию. Заголовок `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.                                                                                                                                           |
| Граница процессов     | RPC на основе разделяемой памяти HQQ. Интерфейс описан в `chrome_net.idl`, который компилируется `hqq-bindgen` в Go-клиент и C-обвязку сервера.                                           |
| `chrome_net_server`   | Небольшой слой на C++ поверх Chromium: один сетевой поток, владеющий всеми объектами `//net`, и потоки сессий, которые блокируются на запросе до тех пор, пока сетевой поток не ответит.       |
| Chromium `//net`      | `URLRequestContext`, `URLRequest`, сессии HTTP/2 и QUIC, BoringSSL.                                                                                                                         |

В IDL шесть операций, которые имеют значение: создание движка из параметров `key=value`, начало запроса с его заголовком (`METHOD URL` и строки заголовков) и длиной тела, запись фрагментов тела запроса, ожидание заголовка ответа, чтение декодированного тела и уничтожение дескрипторов. Дескриптор запроса объявляет `@cancel`, поэтому брошенный вызов прерывает запрос внутри Chromium, а не оставляет его выполняться до конца.

Изоляция стека в рабочем процессе является мерой безопасности не меньше, чем мерой упаковки. Сетевой код Chromium разбирает недоверенные данные из открытого интернета. Если он падает, ваш Go-сервис вместе с ним не падает: вызов завершается с ошибкой, сессия отбрасывается, а клиент запускает новый рабочий процесс при следующей необходимости.

## Воспроизводимая сборка, закрепление по SHA-256

Бинарный файл размером 25 МБ, содержащий Chromium, заслуживает доверия только тогда, когда вы можете точно указать, что в него вошло. `chromenet` следует за Chromium Stable так же, как `gosuda.org/sqlite3` следует за SQLite: исходный код никогда не включается в репозиторий (vendoring), а только закрепляется.

| Закрепление                                                                                                                                  | Где                            |
| :------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------- |
| Релиз Chromium и его официальный архив исходников `-lite`                                                                                    | `third_party/chromium/VERSION` |
| SHA-256 архива и каждого инструментария, закреплённого в этом релизе: clang, Rust, GN, ninja, среда выполнения clang для Windows, xwin       | `third_party/chromium/SHA256`  |
| Версии MSVC CRT и Windows SDK, загружаемые через xwin                                                                                        | `third_party/chromium/WINSDK`  |
| Версия среды выполнения песочницы (`rungv`), дайджест образа гостевой системы, дата снимка Debian                                            | `scripts/engine/BUILDENV`      |
| Среда выполнения C для HQQ                                                                                                                   | `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 с помощью clang-cl и lld-link от Chromium с использованием Windows SDK, который xwin загружает из Microsoft. Движки для macOS собираются нативно на раннере macOS, поскольку сборка Chromium для macOS требует Xcode.

Пересборка того же коммита давала побайтово идентичные бинарные файлы в разных запусках CI. Релизные бинарные файлы побайтово совпадают с файлами, которые собрал более ранний запуск CI для коммита, добавившего движок в монорепозиторий, на всех шести платформах.

| Платформа       | Размер бинарного файла |
| :-------------- | ---------------------: |
| `linux/amd64`   |                27.9 МБ |
| `linux/arm64`   |                27.4 МБ |
| `darwin/amd64`  |                26.2 МБ |
| `darwin/arm64`  |                23.9 МБ |
| `windows/amd64` |                27.3 МБ |
| `windows/arm64` |                25.8 МБ |

Движки для Linux зависят только от `libc`, `libm`, `libdl`, `libpthread` и `libgcc_s` и требуют glibc 2.31 или новее.

### Отслеживание Chromium и релизов

По расписанию запускается рабочий процесс, который проверяет Chromium Stable каждые шесть часов. При появлении нового релиза он переписывает закрепления, собирает и тестирует все шесть движков в ветке `chromium/<version>`, подписывает бинарные файлы аттестацией и коммитит закрепления вместе со встроенными бинарными файлами, их дайджестами и текстами лицензий в `main` одним коммитом. В монорепозитории файлы `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` — это клиент RFC 6455 на чистом Go, он не использует стек Chromium.
- `DialWebTransport` — реализация внутри процесса. Она не обращается к серверу.
- JA4 и отпечатки HTTP/2 по умолчанию соответствуют закреплённому релизу Chromium. Chromium меняет их от релиза к релизу, и `chromenet` следует за ними.
- Бинарные файлы для macOS подписаны ad-hoc и не прошли нотаризацию.
- Рабочий процесс расходует память. Каждый бинарный файл движка занимает на диске от 24 до 28 МБ, а среда выполнения Chromium запускает пул потоков и сетевой поток.
- Сессия движка удерживается до закрытия тела ответа. Программа, которая не закрывает тела ответов, исчерпает пул.

## Источник

Репозиторий находится по адресу [github.com/gosuda/chromenet](https://github.com/gosuda/chromenet) и зеркалирует `//net/chromenet` из монорепозитория gosuda. Рецепт сборки находится в `scripts/engine`, обвязка движка в `engine/`, а интерфейс в `chrome_net.idl`.
