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) и комуникира с работния процес чрез HQQ RPC през споделена памет.
1go get gosuda.org/chromenet@v0.1.10157081033800
Защо да се използва мрежовият стек на Chromium
Go програма, която комуникира с уеб чрез net/http, се представя пред сървъра като Go клиент. Това е правилният избор по подразбиране за трафик между услуги. Той е грешен, когато поведението на сървъра зависи от това за какъв тип клиент той смята, че комуникира: крайни точки, защитени от браузър, anti-bot middleware, CDN мрежи, които предоставят различни версии на протоколи според различните TLS и HTTP/2 fingerprints, и тестове, които трябва да възпроизвеждат това, което изпитва реален браузър.
Стандартната библиотека на Go не може да осигури мрежовото поведение на браузър. crypto/tls има собствен ред на cipher suites и собствен набор от разширения, а net/http има собствени HTTP/2 настройки. Библиотеките, които имитират браузър чрез копиране на байтовете на ClientHello, продължават да копират при всяко ново издание. chromenet следва друг подход: не имитира мрежовия стек на Chromium, а го изпълнява.
Измерено на darwin/arm64 engine спрямо публична услуга за fingerprint (tls.peet.ws), конфигурацията по подразбиране отчита следното:
| Свойство | Наблюдавана стойност |
|---|---|
| Договорен протокол | h2 |
| JA4 | t13d1516h2_8daaf6152771_806a8c22fdea |
| HTTP/2 fingerprint (Akamai format) | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
| User-Agent | десктоп Chrome с основната версия на engine-а, напр. Chrome/157.0.0.0 |
С ForceHTTP3 заявките към cloudflare-quic.com и www.google.com получиха отговор по HTTP/3. Тези стойности са получени на една платформа в един ден. Engine-ът е самият стек, затова те се променят, когато се промени закрепеното издание на 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 не следва пренасочвания и не съхранява бисквитки.
http.Clientправи и двете, както и приhttp.Transport. - Отговорите, компресирани с gzip, deflate, Brotli или zstd, пристигат декодирани, а
resp.Uncompressedе true. - Една заявка държи една engine сесия, докато тялото на отговора не бъде затворено. Затваряйте телата.
- Отмяната на контекста на заявката отменя заявката в 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 | Заменя по подразбиране 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 за всеки origin, без да чака обява в Alt-Svc. |
InsecureSkipVerify | Приема всеки сертификат на сървъра. |
Pool | Engine сесиите, които да се използват. Nil използва client.DefaultPool(), който позволява 64 едновременни заявки. |
Transports с еднакви опции споделят един Chromium URLRequestContext, така че connection pools, HTTP/2 сесии и QUIC сесии се използват повторно между заявките. Задаването на HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server заменя вградения работен процес, например с локален build.
Как работи
| Слой | Какво се изпълнява там |
|---|---|
| Вашата програма | http.Client и chromenet.Transport, чист Go. |
| Граница на процеса | HQQ RPC през споделена памет. Интерфейсът е chrome_net.idl, компилиран от hqq-bindgen в Go клиента и C server glue. |
chrome_net_server | Малък C++ слой върху Chromium: една мрежова нишка, която притежава всички //net обекти, и нишки за сесии, които блокират при заявка, докато мрежовата нишка не отговори. |
Chromium //net | URLRequestContext, URLRequest, HTTP/2 и QUIC сесии, BoringSSL. |
IDL има шест важни операции: създаване на engine от опции key=value, стартиране на заявка с нейния заглавен блок (METHOD URL плюс редове със заглавия) и дължина на тялото, запис на части от тялото на заявката, изчакване на заглавния блок на отговора, четене на декодираното тяло и унищожаване на дръжките. Дръжката на заявката декларира @cancel, така че прекъснато извикване прекратява заявката в Chromium, вместо тя да продължи до край.
Изолирането на стека в работен процес е свойство за безопасност, не само въпрос на опаковане. Мрежовият код на Chromium обработва враждебни входни данни от открития интернет. Ако се срине, вашата Go услуга не се срива заедно с него: извикването завършва с грешка, сесията се изхвърля, а клиентът стартира нов работен процес следващия път, когато е необходим.
Възпроизводимо изграждане, закрепено чрез SHA-256
Двоичен файл от 25 MB, който съдържа Chromium, е надежден само ако можете точно да кажете какво е вложено в него. chromenet следва Chromium Stable по същия начин, по който gosuda.org/sqlite3 следва SQLite: изходният код никога не се вендорира, а само се закрепва.
| Закрепване | Къде се намира |
|---|---|
Издание на Chromium и неговият официален -lite архив с изходния код | third_party/chromium/VERSION |
| SHA-256 на архива и на всеки инструментариум, закрепен от това издание: clang, Rust, GN, ninja, Windows clang runtime, xwin | third_party/chromium/SHA256 |
| Версии на MSVC CRT и Windows SDK, изтеглени през xwin | third_party/chromium/WINSDK |
Издание на sandbox runtime (rungv), дайджест на guest образа, дата на снимката на Debian | scripts/engine/BUILDENV |
| C runtime на HQQ | go.mod и go.sum |
Текущото закрепване е Chromium 157.0.8103.38. Изтегляне, което не съвпада със закрепването си, спира изграждането. Изданието прилага точно една корекция към Chromium: на Linux chrome_net_server се изгражда без NSS и проверява сертификатите на сървъра само спрямо Chrome Root Store, така че двоичният файл не се нуждае от нищо друго освен libc.
Engine-ите за Linux и Windows се изграждат в gvx sandbox среди: непривилегировани процеси с закрепен guest образ и без host daemon. Engine-ите за Windows се кросскомпилират на Linux с clang-cl и lld-link на Chromium срещу Windows SDK, който xwin изтегля от Microsoft. Engine-ите за macOS се изграждат нативно на macOS runner, защото изграждането на Chromium за macOS изисква Xcode.
Повторното изграждане на същия commit даде побайтово идентични двоични файлове в отделни CI изпълнения. Двоичните файлове на издаденото издание са побайтово идентични с тези, изградени от по-ранно CI изпълнение за commit-а, който е въвел engine-а в монорепото, на всичките шест платформи.
| Платформа | Размер на двоичния файл |
|---|---|
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 |
Engine-ите за Linux зависят само от libc, libm, libdl, libpthread и libgcc_s и изискват glibc 2.31 или по-нова версия.
Проследяване на Chromium и издания
Планиран workflow проверява Chromium Stable на всеки шест часа. За ново издание той пренаписва закрепванията, изгражда и тества всичките шест engine-а в клон chromium/<version>, атестира двоичните файлове и комитва закрепванията заедно с вградените двоични файлове, техните дайджести и текстовете на лицензите в main с един commit. В монорепото файловете OWNERS дават на ботa на workflow-а достъп точно до тези файлове, така че актуализацията се внася без човешка стъпка, докато промените в самата рецепта за изграждане (закрепването на Windows SDK и корекцията) все още изискват собственик.
Всяко издадено двоично съдържа атестация за произход на билда от workflow-а, който го е изградил:
1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet
Текстовете на лицензите за всичко, което съдържа всеки двоичен файл (Chromium, BoringSSL, QUICHE и останалата част от build графа), се генерират от tools/licenses/licenses.py на самия Chromium и се разпространяват с изданието като NOTICE_<os>_<arch>.txt.
Ограничения
DialWebSocketе чист Go клиент по RFC 6455 и не използва стека на Chromium.DialWebTransportе имплементация в същия процес. Той не достига до сървър.- JA4 и HTTP/2 fingerprints по подразбиране са тези на закрепеното издание на Chromium. Chromium ги променя от издание към издание, а
chromenetги следва. - macOS двоичните файлове са подписани ad-hoc и не са нотаризирани.
- Работният процес харчи памет. Всеки engine двоичен файл е с размер 24 до 28 MB на диска, а runtime-ът на Chromium стартира пул от нишки и мрежова нишка.
- Engine сесия се държи, докато тялото на отговора не бъде затворено. Програма, която не затваря телата на отговорите, ще изчерпи пула.
Източник
Хранилището е github.com/gosuda/chromenet, огледално копие на //net/chromenet в монорепото gosuda. Рецептата за изграждане се намира в scripts/engine, engine glue кодът в engine/, а интерфейсът в chrome_net.idl.