gosuda.org/chromenet: сетевой стек Chromium для Go, собранный воспроизводимым способом из зафиксированного релиза

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.
1go 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:
1package main
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7
8 "gosuda.org/chromenet"
9)
10
11func main() {
12 client := &http.Client{Transport: chromenet.NewTransport(chromenet.Options{})}
13
14 resp, err := client.Get("https://example.com/")
15 if err != nil {
16 panic(err)
17 }
18 defer resp.Body.Close()
19
20 body, _ := io.ReadAll(resp.Body)
21 fmt.Println(resp.Proto, resp.StatusCode, len(body))
22}
При первом запросе модуль записывает встроенный рабочий бинарный файл для вашей платформы в каталог пользовательского кэша, проверяет его 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:
1var ne *client.NetError
2if errors.As(err, &ne) {
3 fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}
Параметры
| Поле | Эффект |
|---|---|
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: непривилегированных процессов с закреплённым образом гостевой системы и без демона на хосте. Движки для 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 и патч) по-прежнему требуют подтверждения владельца.
Каждый релизный бинарный файл несёт аттестацию происхождения сборки от рабочего процесса, который его собрал:
1gh 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 и зеркалирует //net/chromenet из монорепозитория gosuda. Рецепт сборки находится в scripts/engine, обвязка движка в engine/, а интерфейс в chrome_net.idl.